Skip to main content

ravenrs_widgets/grid/
view.rs

1//! The data grid view: toolbar, group panel, header, filter row, virtualized
2//! body and footer.
3
4use std::cell::{Cell, RefCell};
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::rc::Rc;
8
9use ravenrs_core::{Color, CursorIcon, Insets, Key, Point, Quad, Rect, Role};
10use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
11use ravenrs_reactive::{Binding, State};
12use ravenrs_style::{Semantic, TextStyle, Theme, Weight};
13
14use super::layout::{self, Arrangement, GridLayout, GridScenario};
15use super::model::{CellAlign, GridColumn, GridData, Row, SortDirection, Summary, Value};
16use super::query::{
17    GridExportFormat, GridQuery, GridTable, QueryResult, ViewRow, check_formula, formula_value, group_ranges,
18    run_query, to_csv, to_table, to_tsv,
19};
20use super::remote::RemoteRows;
21use crate::context::{BuildCx, DragPhase, NodeId, Paint, Placement, QuadPaint, Semantics};
22use crate::overlay::{Entry, MenuList, PanelOf};
23use crate::{
24    AnyView, Badge, Button, ButtonSize, ButtonStyle, Dialog, DropdownMenu, IconName, MenuItem, Pagination,
25    Select, Spacer, TextField, View, util,
26};
27
28/// How rows can be selected.
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30#[non_exhaustive]
31#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
32pub enum SelectionMode {
33    /// Rows cannot be selected.
34    None,
35    /// One row at a time; arrow keys move the selection.
36    #[default]
37    Single,
38    /// Check boxes; Ctrl/Cmd-click toggles, Shift-click selects a range.
39    Multiple,
40}
41
42/// A virtualized data grid in the spirit of enterprise grids: sorting by one or
43/// several columns, a filter row with operators and filters by value, a search panel,
44/// grouping by dragging columns to the group panel, group summaries, footer totals,
45/// columns to resize, reorder, pin and hide, a menu on each column, single or multiple
46/// selection, keyboard navigation, in-place editing, cell templates, CSV export, pages,
47/// saved scenarios, and rows that stay on a server ([`DataGrid::remote`]). Only the
48/// visible rows are built, so 100,000+ rows stay smooth.
49///
50/// ```
51/// use ravenrs_widgets::{DataGrid, GridColumn, GridData, SortDirection, Summary, Ui};
52///
53/// let data = GridData::new(vec![
54///     vec!["Acme".into(), 1200.0.into()],
55///     vec!["Globex".into(), 860.5.into()],
56/// ]);
57/// let grid = DataGrid::new(
58///     data,
59///     [GridColumn::text("Customer"), GridColumn::currency("MRR", "$", 2).summary(Summary::Sum)],
60/// )
61/// .sort_by(1, SortDirection::Descending)
62/// .height(320.0);
63///
64/// let mut ui = Ui::new(grid, ravenrs_style::Theme::raven_dark());
65/// ui.update();
66/// ```
67#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
68pub struct DataGrid {
69    empty_text: Option<String>,
70    data: GridData,
71    columns: Vec<GridColumn>,
72    selection_mode: SelectionMode,
73    selection: Option<Binding<BTreeSet<usize>>>,
74    initial: GridQuery,
75    filter_row: bool,
76    search_panel: bool,
77    toolbar: bool,
78    footer: bool,
79    height: Option<f32>,
80    row_height: f32,
81    striped: bool,
82    even_row: Option<Color>,
83    odd_row: Option<Color>,
84    horizontal_lines: bool,
85    on_export: Option<Rc<dyn Fn(String)>>,
86    on_row_activate: Option<Rc<dyn Fn(usize)>>,
87    remote: Option<RemoteRows>,
88    group_panel: Option<bool>,
89    page_size: Option<usize>,
90    vertical_lines: bool,
91    layout: Option<Binding<GridLayout>>,
92    scenarios: Option<Binding<Vec<GridScenario>>>,
93    key_column: Option<usize>,
94    selected_keys: Option<Binding<BTreeSet<String>>>,
95    on_cell_edit: Option<CellEditHandler>,
96    row_actions: Vec<GridRowAction>,
97    on_add_row: Option<Rc<dyn Fn()>>,
98    on_delete_rows: Option<Rc<dyn Fn(Vec<usize>)>>,
99    row_detail: Option<RowDetail>,
100    detail_height: f32,
101    expanded_rows: Option<Binding<BTreeSet<usize>>>,
102    on_export_file: Option<Rc<dyn Fn(GridExportFormat, GridTable)>>,
103    export_title: Option<String>,
104    filter_builder: bool,
105    cell_style: Option<CellStyler>,
106    name_column: Option<usize>,
107}
108
109type CellStyler = Rc<dyn Fn(&Row, usize) -> GridCellStyle>;
110type RowDetail = Rc<dyn Fn(&Row, usize) -> AnyView>;
111type CellEditHandler = Rc<dyn Fn(&CellEdit) -> Result<(), String>>;
112type RowTest = Rc<dyn Fn(&Row) -> bool>;
113
114/// Room for one button of a [`GridRowAction`].
115/// How much of the theme's control color a shaded row shows.
116const STRIPED_ROW: f32 = 0.4;
117/// The same under the pointer, always more than a shaded row.
118const HOVERED_ROW: f32 = 0.85;
119/// How much accent a selected row shows, always more than either.
120const SELECTED_ROW: f32 = 0.13;
121const ROW_ACTION_WIDTH: f32 = 34.0;
122/// Room for the arrow that opens a row's details, and for a row's check box.
123const DETAIL_ARROW_WIDTH: f32 = 32.0;
124const CHECK_WIDTH: f32 = 44.0;
125
126/// Where rows stand when some have their details open below them. With none open, a
127/// row is just its number of row heights down.
128#[derive(Clone, Debug, Default, PartialEq)]
129struct RowTops {
130    row_height: f32,
131    detail_height: f32,
132    /// The rows with details open, in order.
133    open: Vec<usize>,
134}
135
136#[cfg_attr(feature = "live", ravenrs_live::expose)]
137impl RowTops {
138    fn top(&self, row: usize) -> f32 {
139        row as f32 * self.row_height + self.open.partition_point(|&o| o < row) as f32 * self.detail_height
140    }
141
142    /// The row covering this height, or the one its details belong to.
143    fn row_at(&self, y: f32, len: usize) -> usize {
144        let (mut low, mut high) = (0, len);
145        while low < high {
146            let middle = (low + high) / 2;
147            if self.top(middle + 1) <= y {
148                low = middle + 1;
149            } else {
150                high = middle;
151            }
152        }
153        low
154    }
155}
156
157/// How a cell looks, by what its row holds. Colors come from the theme.
158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
159#[non_exhaustive]
160#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
161pub struct GridCellStyle {
162    /// The color of the text.
163    pub ink: Option<Semantic>,
164    /// A faint color behind it.
165    pub fill: Option<Semantic>,
166    /// Heavier text.
167    pub bold: bool,
168}
169
170#[cfg_attr(feature = "live", ravenrs_live::expose)]
171impl GridCellStyle {
172    /// A cell with this color of text, this color behind it, and heavier text or not.
173    pub fn new(ink: Option<Semantic>, fill: Option<Semantic>, bold: bool) -> Self {
174        Self { ink, fill, bold }
175    }
176}
177
178/// A change someone made in a cell, before it is kept.
179#[derive(Clone, Debug, PartialEq)]
180#[non_exhaustive]
181#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
182pub struct CellEdit {
183    /// The row in the data.
184    pub row: usize,
185    /// The column.
186    pub column: usize,
187    /// What the cell held.
188    pub previous: Value,
189    /// What it would hold now.
190    pub value: Value,
191}
192
193/// A button at the end of every row, such as Edit or Delete.
194#[derive(Clone)]
195#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
196pub struct GridRowAction {
197    label: String,
198    icon: IconName,
199    on_click: Rc<dyn Fn(&Row)>,
200    enabled: Option<RowTest>,
201}
202
203impl fmt::Debug for GridRowAction {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        f.debug_struct("GridRowAction").field("label", &self.label).finish_non_exhaustive()
206    }
207}
208
209#[cfg_attr(feature = "live", ravenrs_live::expose)]
210impl GridRowAction {
211    /// A button with this icon that runs `on_click` with its row. Screen readers hear
212    /// `label` and the row, such as "Delete Acme", from
213    /// [`key_column`](DataGrid::key_column) or else the first column.
214    pub fn new(label: impl Into<String>, icon: IconName, on_click: impl Fn(&Row) + 'static) -> Self {
215        Self { label: label.into(), icon, on_click: Rc::new(on_click), enabled: None }
216    }
217
218    /// Turns the button off on the rows where this says false.
219    pub fn enabled(mut self, enabled: impl Fn(&Row) -> bool + 'static) -> Self {
220        self.enabled = Some(Rc::new(enabled));
221        self
222    }
223}
224
225impl fmt::Debug for DataGrid {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.debug_struct("DataGrid")
228            .field("columns", &self.columns)
229            .field("rows", &self.data.len())
230            .finish_non_exhaustive()
231    }
232}
233
234#[cfg_attr(feature = "live", ravenrs_live::expose)]
235impl DataGrid {
236    /// What it says when there is no data at all, as opposed to filters leaving nothing.
237    pub fn empty_text(mut self, text: impl Into<String>) -> Self {
238        self.empty_text = Some(text.into());
239        self
240    }
241
242    /// A grid over `data` with these columns; single selection, filter row, toolbar and footer on.
243    pub fn new(data: GridData, columns: impl IntoIterator<Item = GridColumn>) -> Self {
244        Self {
245            empty_text: None,
246            data,
247            columns: columns.into_iter().collect(),
248            selection_mode: SelectionMode::Single,
249            selection: None,
250            initial: GridQuery::default(),
251            filter_row: true,
252            search_panel: true,
253            toolbar: true,
254            footer: true,
255            height: None,
256            row_height: 36.0,
257            striped: false,
258            even_row: None,
259            odd_row: None,
260            horizontal_lines: true,
261            on_export: None,
262            on_row_activate: None,
263            remote: None,
264            group_panel: None,
265            page_size: None,
266            vertical_lines: false,
267            layout: None,
268            scenarios: None,
269            key_column: None,
270            selected_keys: None,
271            on_cell_edit: None,
272            row_actions: Vec::new(),
273            on_add_row: None,
274            on_delete_rows: None,
275            row_detail: None,
276            detail_height: 240.0,
277            expanded_rows: None,
278            on_export_file: None,
279            export_title: None,
280            filter_builder: false,
281            cell_style: None,
282            name_column: None,
283        }
284    }
285
286    /// The column whose cell names a row for screen readers, in the buttons of
287    /// [`row_action`](Self::row_action). The key column, or else the first, by default.
288    pub fn name_column(mut self, column: usize) -> Self {
289        self.name_column = Some(column);
290        self
291    }
292
293    /// Decides how each cell looks, from its row and its column.
294    pub fn cell_style(mut self, style: impl Fn(&Row, usize) -> GridCellStyle + 'static) -> Self {
295        self.cell_style = Some(Rc::new(style));
296        self
297    }
298
299    /// Shows a Filter button in the toolbar that opens a [`FilterBuilder`](crate::FilterBuilder)
300    /// over the columns. What it writes is an expression, kept with the filters of
301    /// the filter row, so it goes in saved scenarios and comes out with Clear.
302    pub fn filter_builder(mut self, show: bool) -> Self {
303        self.filter_builder = show;
304        self
305    }
306
307    /// Gives every row details of its own, opened by an arrow at its start or with the Right
308    /// key: a nested list, a form, a chart. The view is built only while it is open,
309    /// with the row and its index in the data.
310    pub fn row_detail(mut self, detail: impl Fn(&Row, usize) -> AnyView + 'static) -> Self {
311        self.row_detail = Some(Rc::new(detail));
312        self
313    }
314
315    /// The height of a row's details in logical pixels. 240 by default.
316    pub fn detail_height(mut self, height: f32) -> Self {
317        self.detail_height = height.max(1.0);
318        self
319    }
320
321    /// The rows with details open, by index in the data, to keep or to set from outside.
322    pub fn expanded_rows(mut self, rows: Binding<BTreeSet<usize>>) -> Self {
323        self.expanded_rows = Some(rows);
324        self
325    }
326
327    /// Shows an Export menu with Excel, PDF and CSV; `handler` receives the format and the rows
328    /// that match, as shown. `ravenrs-grid-export` turns these into a file.
329    pub fn on_export_file(mut self, handler: impl Fn(GridExportFormat, GridTable) + 'static) -> Self {
330        self.on_export_file = Some(Rc::new(handler));
331        self
332    }
333
334    /// The title of the files the Export menu makes. The grid's name by default.
335    pub fn export_title(mut self, title: impl Into<String>) -> Self {
336        self.export_title = Some(title.into());
337        self
338    }
339
340    /// A grid over rows that stay on a server. It asks for what is on the screen, keeps
341    /// a limited number of rows, and sorting, filtering and searching are up to the
342    /// loader. It scrolls on as rows arrive, or in pages with
343    /// [`page_size`](Self::page_size); grouping needs the rows at hand, so it is off.
344    pub fn remote(rows: RemoteRows, columns: impl IntoIterator<Item = GridColumn>) -> Self {
345        let mut grid = Self::new(GridData::new(Vec::new()), columns);
346        grid.remote = Some(rows);
347        grid.group_panel = Some(false);
348        grid
349    }
350
351    /// Shows the rows a page at a time, with a pager at the bottom.
352    pub fn page_size(mut self, rows: usize) -> Self {
353        self.page_size = Some(rows.max(1));
354        self
355    }
356
357    /// Shows or hides the space where columns are dropped to group by them. It shows
358    /// along with the toolbar, unless this says otherwise.
359    pub fn group_panel(mut self, show: bool) -> Self {
360        self.group_panel = Some(show);
361        self
362    }
363
364    /// Draws a line between columns.
365    pub fn vertical_lines(mut self, show: bool) -> Self {
366        self.vertical_lines = show;
367        self
368    }
369
370    /// Where its arrangement lives: read when it changes, and written when someone moves
371    /// something. Keep this to bring it back as it was.
372    pub fn layout(mut self, layout: Binding<GridLayout>) -> Self {
373        self.layout = Some(layout);
374        self
375    }
376
377    /// Offers saving its arrangement under a name and coming back to it. Keep the list
378    /// with [`GridScenario::list_to_json`].
379    pub fn scenarios(mut self, scenarios: Binding<Vec<GridScenario>>) -> Self {
380        self.scenarios = Some(scenarios);
381        self
382    }
383
384    /// Sets how rows can be selected (single by default).
385    pub fn selection_mode(mut self, mode: SelectionMode) -> Self {
386        self.selection_mode = mode;
387        self
388    }
389
390    /// Where the selected row indices live (otherwise the grid keeps them).
391    pub fn selection(mut self, selection: Binding<BTreeSet<usize>>) -> Self {
392        self.selection = Some(selection);
393        self
394    }
395
396    /// The column that identifies a record, for [`selected_keys`](Self::selected_keys)
397    /// and the names of [`GridRowAction`]s.
398    pub fn key_column(mut self, column: usize) -> Self {
399        self.key_column = Some(column);
400        self
401    }
402
403    /// Where the keys of the selected rows live, with
404    /// [`key_column`](Self::key_column). Unlike row indices, keys still mean the same
405    /// records after the data is replaced.
406    pub fn selected_keys(mut self, keys: Binding<BTreeSet<String>>) -> Self {
407        self.selected_keys = Some(keys);
408        self
409    }
410
411    /// Double-click or Enter on a row, with the row's values. Works the same over
412    /// rows at hand and rows on a server.
413    pub fn on_row_open(mut self, handler: impl Fn(&Row) + 'static) -> Self {
414        let (data, remote) = (self.data.clone(), self.remote.clone());
415        self.on_row_activate = Some(Rc::new(move |index| {
416            let row = match &remote {
417                Some(remote) => remote.row(index),
418                None => data.rows().get(index).cloned(),
419            };
420            if let Some(row) = row {
421                handler(&row);
422            }
423        }));
424        self
425    }
426
427    /// Sees each edit before it is kept. An error stays on show, in the toolbar, and the
428    /// cell stays open. With it, columns marked [`editable`](GridColumn::editable) can
429    /// be edited with a double-click over rows on a server too: the handler saves them.
430    pub fn on_cell_edit(mut self, handler: impl Fn(&CellEdit) -> Result<(), String> + 'static) -> Self {
431        self.on_cell_edit = Some(Rc::new(handler));
432        self
433    }
434
435    /// Adds a button at the end of every row.
436    pub fn row_action(mut self, action: GridRowAction) -> Self {
437        self.row_actions.push(action);
438        self
439    }
440
441    /// Shows an Add button in the toolbar. The handler adds the row, or opens a form to.
442    pub fn on_add_row(mut self, handler: impl Fn() + 'static) -> Self {
443        self.on_add_row = Some(Rc::new(handler));
444        self
445    }
446
447    /// Lets the Delete key and a toolbar button remove the selected rows. The handler
448    /// receives them, and it decides: it may ask first.
449    pub fn on_delete_rows(mut self, handler: impl Fn(Vec<usize>) + 'static) -> Self {
450        self.on_delete_rows = Some(Rc::new(handler));
451        self
452    }
453
454    /// Initial sort; call again to add more sort columns.
455    pub fn sort_by(mut self, column: usize, direction: SortDirection) -> Self {
456        self.initial.sort.push((column, direction));
457        self
458    }
459
460    /// Initial grouping; call again for nested groups.
461    pub fn group_by(mut self, column: usize) -> Self {
462        self.initial.group_by.push(column);
463        self
464    }
465
466    /// Shows or hides the filter row under the header.
467    pub fn filter_row(mut self, show: bool) -> Self {
468        self.filter_row = show;
469        self
470    }
471
472    /// Shows or hides the search box in the toolbar.
473    pub fn search_panel(mut self, show: bool) -> Self {
474        self.search_panel = show;
475        self
476    }
477
478    /// The toolbar with search, grouping, column chooser and export.
479    pub fn toolbar(mut self, show: bool) -> Self {
480        self.toolbar = show;
481        self
482    }
483
484    /// The footer with column totals.
485    pub fn footer(mut self, show: bool) -> Self {
486        self.footer = show;
487        self
488    }
489
490    /// A fixed height; otherwise the grid fills the space its parent gives it.
491    pub fn height(mut self, height: f32) -> Self {
492        self.height = Some(height);
493        self
494    }
495
496    /// Sets the row height in logical pixels (36 by default, at least 24).
497    pub fn row_height(mut self, height: f32) -> Self {
498        self.row_height = height.max(24.0);
499        self
500    }
501
502    /// Shades every other row with a tint of the theme's control color, which leaves
503    /// the accent to the selection. Counting starts again after each group row.
504    pub fn striped(mut self, striped: bool) -> Self {
505        self.striped = striped;
506        self
507    }
508
509    /// Paints the second, fourth and so on rows in this color, in place of [`Self::striped`].
510    pub fn even_row(mut self, color: Color) -> Self {
511        self.even_row = Some(color);
512        self
513    }
514
515    /// Paints the first, third and so on rows in this color.
516    pub fn odd_row(mut self, color: Color) -> Self {
517        self.odd_row = Some(color);
518        self
519    }
520
521    /// Whether a line separates the rows. On by default.
522    pub fn horizontal_lines(mut self, show: bool) -> Self {
523        self.horizontal_lines = show;
524        self
525    }
526
527    /// Shows an Export button; `handler` receives the matching rows as CSV.
528    pub fn on_export(mut self, handler: impl Fn(String) + 'static) -> Self {
529        self.on_export = Some(Rc::new(handler));
530        self
531    }
532
533    /// Double-click or Enter on a row whose cell is not editable.
534    pub fn on_row_activate(mut self, handler: impl Fn(usize) + 'static) -> Self {
535        self.on_row_activate = Some(Rc::new(handler));
536        self
537    }
538}
539
540struct Cached {
541    summaries: Vec<Option<Summary>>,
542    query: GridQuery,
543    rows: Rc<Vec<Row>>,
544    result: Rc<QueryResult>,
545}
546
547/// Interaction state the grid keeps across rebuilds. Cells written from event
548/// handlers (and read during the next build) use `Cell`s, so writing them does
549/// not trigger an extra rebuild.
550#[derive(Clone)]
551struct Handles {
552    building_filter: State<bool>,
553    expanded: Binding<BTreeSet<usize>>,
554    query: State<GridQuery>,
555    widths: State<Vec<f32>>,
556    selection: Binding<BTreeSet<usize>>,
557    focus_row: State<Option<usize>>,
558    anchor: Rc<Cell<Option<usize>>>,
559    reveal: Rc<Cell<Option<usize>>>,
560    editing: State<Option<(usize, usize)>>,
561    edit_buffer: State<String>,
562    edit_error: State<Option<String>>,
563    resize: Rc<Cell<Option<(usize, f32, f32)>>>,
564    modifiers: Rc<Cell<ravenrs_core::Modifiers>>,
565    arrange: State<Arrangement>,
566    drag: State<Option<GridDrag>>,
567    /// Where a press started, and whether it has become a drag.
568    press: Rc<Cell<(Point, bool)>>,
569    /// A drag just ended, so the click that comes with letting go does not count.
570    dragged: Rc<Cell<bool>>,
571    zones: Rc<RefCell<Zones>>,
572    menu: State<Option<(usize, Option<Point>)>>,
573    filter_open: State<Option<usize>>,
574    filter_chosen: State<BTreeSet<String>>,
575    filter_search: State<String>,
576    page: State<usize>,
577    saving: State<bool>,
578    scenario_name: State<String>,
579    active_scenario: State<Option<String>>,
580    /// The layout as it first was, to come back to.
581    original: Rc<RefCell<Option<GridLayout>>>,
582    seen_layout: Rc<RefCell<Option<GridLayout>>>,
583    /// Which column's summary is being edited.
584    summary_edit: State<Option<usize>>,
585    summary_draft: State<String>,
586    /// What formulas came to, by column and range of rows, until the rows change.
587    formula_cache: Rc<RefCell<FormulaCache>>,
588}
589
590#[derive(Default)]
591struct FormulaCache {
592    /// What the answers are about: the result of the query and the formulas.
593    about: (usize, Vec<Option<String>>),
594    answers: BTreeMap<(usize, usize, usize), String>,
595}
596
597/// Everything a row needs to build itself.
598struct Shared<'a> {
599    grid: &'a DataGrid,
600    rows: &'a [Row],
601    rows_rc: Rc<Vec<Row>>,
602    result: Rc<QueryResult>,
603    visible: &'a [usize],
604    widths: &'a [f32],
605    total_width: f32,
606    select_width: f32,
607    selected: &'a BTreeSet<usize>,
608    focus_row: Option<usize>,
609    editing: Option<(usize, usize)>,
610    h: Handles,
611    arrangement: &'a Arrangement,
612    /// Pinned columns and where each sits from the left of the grid.
613    fixed: Vec<(usize, f32)>,
614    last_fixed: Option<usize>,
615    /// How wide the columns that stay in place are, with the check boxes.
616    fixed_width: f32,
617    offset_x: f32,
618    /// How far the first column of a data row goes in, under its groups.
619    indent: f32,
620    /// Title and whether it groups, for what a drag needs after the build.
621    column_facts: Rc<Vec<(String, bool)>>,
622    /// Where a dragged column would land.
623    drop_before: Option<Option<usize>>,
624    /// Where column lines go, from the left of the row.
625    lines: Rc<Vec<f32>>,
626    /// Where each visible row's group starts in the matching rows, and how long it is.
627    ranges: Rc<Vec<(usize, usize)>>,
628}
629
630#[cfg_attr(feature = "live", ravenrs_live::expose)]
631impl Shared<'_> {
632    fn fixed_x(&self, column: usize) -> Option<f32> {
633        self.fixed.iter().find(|(c, _)| *c == column).map(|(_, x)| *x)
634    }
635
636    /// What a formula comes to over a stretch of the matching rows.
637    fn formula_text(&self, c: usize, range: (usize, usize)) -> Option<String> {
638        let formula = self.arrangement.formulas.get(c)?.as_ref()?;
639        let mut cache = self.h.formula_cache.borrow_mut();
640        let key = (c, range.0, range.1);
641        if let Some(known) = cache.answers.get(&key) {
642            return Some(known.clone());
643        }
644        let columns = &self.grid.columns;
645        let end = (range.0 + range.1).min(self.result.matched.len());
646        let indices = &self.result.matched[range.0.min(end)..end];
647        let answer = check_formula(columns, formula)
648            .and_then(|parsed| formula_value(columns, self.rows, indices, &parsed))
649            .map_or_else(|_| "#!".to_owned(), |value| formula_display(&columns[c], &value));
650        cache.answers.insert(key, answer.clone());
651        Some(answer)
652    }
653
654    /// Where a column starts in the row.
655    fn x_of(&self, column: usize) -> f32 {
656        let mut x = self.select_width;
657        for &c in self.visible {
658            if c == column {
659                return x;
660            }
661            x += self.widths[c];
662        }
663        x
664    }
665}
666
667fn cell_attrs(theme: &Theme) -> ravenrs_text::TextAttrs {
668    util::attrs(theme, TextStyle::Callout, None)
669}
670
671fn justify(align: CellAlign) -> Justify {
672    match align {
673        CellAlign::Start => Justify::Start,
674        CellAlign::Center => Justify::Center,
675        CellAlign::End => Justify::End,
676    }
677}
678
679/// A fixed-width cell box.
680fn cell_style(width: f32, height: f32, align: CellAlign) -> LayoutStyle {
681    LayoutStyle {
682        width: Some(width),
683        height: Some(height),
684        padding: Insets::symmetric(0.0, 12.0),
685        spacing: 6.0,
686        align: Align::Center,
687        justify: justify(align),
688        overflow_hidden: true,
689        ..LayoutStyle::stack(Axis::Horizontal)
690    }
691}
692
693/// Applies a click on data row `row` (at view index `index`) to the selection.
694fn click_row(shared_result: &QueryResult, h: &Handles, mode: SelectionMode, row: usize, index: usize) {
695    let m = h.modifiers.get();
696    h.focus_row.set(Some(index));
697    match mode {
698        SelectionMode::None => {}
699        SelectionMode::Single => h.selection.set(BTreeSet::from([row])),
700        SelectionMode::Multiple => {
701            if m.shift
702                && let Some(anchor) = h.anchor.get()
703            {
704                let (a, b) = if anchor <= index { (anchor, index) } else { (index, anchor) };
705                let range = shared_result.rows[a..=b.min(shared_result.rows.len() - 1)]
706                    .iter()
707                    .filter_map(|r| if let ViewRow::Data(i) = r { Some(*i) } else { None })
708                    .collect();
709                h.selection.set(range);
710                return;
711            } else if m.ctrl || m.meta {
712                h.selection.update(|s| {
713                    if !s.remove(&row) {
714                        s.insert(row);
715                    }
716                });
717            } else {
718                h.selection.set(BTreeSet::from([row]));
719            }
720        }
721    }
722    h.anchor.set(Some(index));
723}
724
725/// Who a grid's drag is moving.
726#[derive(Clone, Copy, Debug, PartialEq, Eq)]
727enum DragSource {
728    /// A column, from its header.
729    Column(usize),
730    /// A grouped column, from its chip.
731    Chip(usize),
732}
733
734/// What letting go would do.
735#[derive(Clone, Copy, Debug, PartialEq, Eq)]
736enum DropTarget {
737    Nothing,
738    /// Group by it, at this place among the groups.
739    Group(usize),
740    /// Show it before this column, or last.
741    Column(Option<usize>),
742    /// Stop grouping by it.
743    Ungroup,
744}
745
746#[derive(Clone, Copy, Debug, PartialEq)]
747struct GridDrag {
748    source: DragSource,
749    at: Point,
750    target: DropTarget,
751}
752
753/// Where things landed in the last paint, to tell what a drag is over.
754#[derive(Default)]
755struct Zones {
756    panel: Rect,
757    header: Rect,
758    chips: BTreeMap<usize, Rect>,
759    cells: BTreeMap<usize, Rect>,
760}
761
762/// What the pointer has to travel before a press becomes a drag, and not a click.
763const DRAG_FROM: f32 = 5.0;
764const HEADER_HEIGHT: f32 = 38.0;
765const FOOTER_HEIGHT: f32 = 40.0;
766const FOOTER_NAME_FROM: f32 = 170.0;
767const SUMMARY_EDITOR_WIDTH: f32 = 470.0;
768const GROUP_INDENT: f32 = 20.0;
769const GROUP_PANEL_HEIGHT: f32 = 44.0;
770/// Below this many distinct values a filter menu lists them all.
771const MAX_FILTER_VALUES: usize = 500;
772/// Roughly how wide a character of a cell is, to fit a column to its content.
773const CHAR_WIDTH: f32 = 7.2;
774const PAGE_SIZES: [usize; 5] = [25, 50, 100, 200, 500];
775
776/// A press on a header or a chip: it follows the pointer once it is a drag, and on
777/// letting go does what `target` says.
778fn drag_handler(
779    h: &Handles,
780    columns: &Rc<Vec<(String, bool)>>,
781    source: DragSource,
782) -> Rc<dyn Fn(crate::Drag)> {
783    let (h, columns) = (h.clone(), columns.clone());
784    Rc::new(move |d| match d.phase {
785        DragPhase::Start => {
786            h.press.set((d.position, false));
787            h.dragged.set(false);
788        }
789        DragPhase::Move => {
790            let (start, active) = h.press.get();
791            if !active {
792                let (dx, dy) = (d.position.x - start.x, d.position.y - start.y);
793                if dx.hypot(dy) < DRAG_FROM {
794                    return;
795                }
796                h.press.set((start, true));
797                h.dragged.set(true);
798                h.drag.set(Some(GridDrag { source, at: d.position, target: DropTarget::Nothing }));
799            }
800            // Escape took it away while it was being dragged.
801            if h.drag.peek().is_none() {
802                return;
803            }
804            let target = drop_target(&h, &columns, source, d.position);
805            h.drag.set(Some(GridDrag { source, at: d.position, target }));
806        }
807        _ => {
808            let (_, active) = h.press.get();
809            h.press.set((d.position, false));
810            let drag = *h.drag.peek();
811            if active && let Some(drag) = drag {
812                h.dragged.set(true);
813                h.drag.set(None);
814                finish_drag(&h, drag);
815            }
816        }
817    })
818}
819
820fn drop_target(h: &Handles, columns: &[(String, bool)], source: DragSource, at: Point) -> DropTarget {
821    let zones = h.zones.borrow();
822    if zones.panel.contains(at) {
823        if let DragSource::Column(c) = source
824            && !columns.get(c).is_some_and(|col| col.1)
825        {
826            return DropTarget::Nothing;
827        }
828        let mut place = 0;
829        let moving = if let DragSource::Chip(c) = source { Some(c) } else { None };
830        for (i, &group) in h.query.peek().group_by.iter().enumerate() {
831            if Some(group) == moving {
832                continue;
833            }
834            let past = zones.chips.get(&i).is_some_and(|chip| at.x > chip.center().x);
835            if past {
836                place += 1;
837            }
838        }
839        return DropTarget::Group(place);
840    }
841    if zones.header.contains(at) {
842        let before = zones
843            .cells
844            .iter()
845            .filter(|(_, r)| at.x < r.center().x)
846            .min_by(|a, b| a.1.x().partial_cmp(&b.1.x()).unwrap_or(std::cmp::Ordering::Equal));
847        return DropTarget::Column(before.map(|(c, _)| *c));
848    }
849    match source {
850        DragSource::Chip(_) => DropTarget::Ungroup,
851        DragSource::Column(_) => DropTarget::Nothing,
852    }
853}
854
855fn finish_drag(h: &Handles, drag: GridDrag) {
856    let column = match drag.source {
857        DragSource::Column(c) | DragSource::Chip(c) => c,
858    };
859    match drag.target {
860        DropTarget::Nothing => {}
861        DropTarget::Group(place) => h.query.update(|q| {
862            q.group_by.retain(|g| *g != column);
863            let place = place.min(q.group_by.len());
864            q.group_by.insert(place, column);
865            q.collapsed.clear();
866        }),
867        DropTarget::Column(before) => {
868            if matches!(drag.source, DragSource::Chip(_)) {
869                h.query.update(|q| q.toggle_group(column));
870            }
871            h.arrange.update(|a| a.move_column(column, before));
872        }
873        DropTarget::Ungroup => h.query.update(|q| q.toggle_group(column)),
874    }
875}
876
877/// The thin handle at a header's edge: drag to resize, double-click to fit.
878struct ResizeHandle<'a> {
879    column: usize,
880    shared: &'a Shared<'a>,
881}
882
883impl View for ResizeHandle<'_> {
884    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
885        let theme = *cx.theme();
886        let (s, c) = (self.shared, self.column);
887        let col = &s.grid.columns[c];
888        let shown = cx.is_hovered() || s.h.resize.get().is_some_and(|(column, ..)| column == c);
889        let line = theme.color(Semantic::Accent);
890        let handle = cx.container(
891            LayoutStyle { width: Some(6.0), height: Some(HEADER_HEIGHT), ..LayoutStyle::leaf() },
892            &[],
893            Paint::Custom(Rc::new(move |f: Rect, scene| {
894                if shown {
895                    scene.push_quad(Quad::new(
896                        Rect::new(f.center().x - 1.0, f.y() + 6.0, 2.0, f.height() - 12.0),
897                        line,
898                    ));
899                }
900            })),
901        );
902        let (widths, resize, min) = (s.h.widths.clone(), s.h.resize.clone(), col.min_width);
903        cx.on_drag(
904            handle,
905            Rc::new(move |d| match d.phase {
906                DragPhase::Start => {
907                    let w = widths.peek().get(c).copied().unwrap_or(120.0);
908                    resize.set(Some((c, d.position.x, w)));
909                }
910                DragPhase::Move => {
911                    if let Some((col, start_x, start_w)) = resize.get()
912                        && col == c
913                    {
914                        let w = (start_w + d.position.x - start_x).max(min);
915                        widths.update(|ws| ws[c] = w);
916                    }
917                }
918                _ => resize.set(None),
919            }),
920        );
921        let (widths, width) = (s.h.widths.clone(), fit_width(s, c));
922        cx.on_double_click(handle, Rc::new(move || widths.update(|ws| ws[c] = width)));
923        cx.cursor(handle, CursorIcon::ColResize);
924        handle
925    }
926}
927
928/// The width that shows a column's title and what is on the screen of its values.
929fn fit_width(s: &Shared<'_>, c: usize) -> f32 {
930    let col = &s.grid.columns[c];
931    let empty = Value::Empty;
932    let longest = s
933        .rows
934        .iter()
935        .take(2000)
936        .map(|row| col.display_cow(row.get(c).unwrap_or(&empty)).chars().count())
937        .max()
938        .unwrap_or(0)
939        .max(col.title.chars().count() + 4);
940    (longest as f32 * CHAR_WIDTH + 32.0).max(col.min_width)
941}
942
943/// What a column's menu holds.
944fn column_menu(s: &Shared<'_>, c: usize, labels: &crate::Labels) -> Vec<Entry> {
945    let (col, query, h) = (&s.grid.columns[c], s.h.query.peek().clone(), &s.h);
946    let mut entries = Vec::new();
947    let item = |text: &str, icon: IconName| MenuItem::new(text.to_owned()).icon(icon);
948    if col.sortable {
949        for (text, icon, direction) in [
950            (&labels.grid_sort_ascending, IconName::ArrowUp, SortDirection::Ascending),
951            (&labels.grid_sort_descending, IconName::ArrowDown, SortDirection::Descending),
952        ] {
953            let q = h.query.clone();
954            entries.push(Entry::Item(item(text, icon).on_select(move || {
955                q.update(|q| {
956                    q.sort.retain(|(sorted, _)| *sorted != c);
957                    q.sort.insert(0, (c, direction));
958                });
959            })));
960        }
961        if query.sort.iter().any(|(sorted, _)| *sorted == c) {
962            let q = h.query.clone();
963            entries.push(Entry::Item(
964                item(&labels.grid_clear_sort, IconName::X)
965                    .on_select(move || q.update(|q| q.sort.retain(|(sorted, _)| *sorted != c))),
966            ));
967        }
968        entries.push(Entry::Separator);
969    }
970    if col.groupable && s.grid.remote.is_none() {
971        let q = h.query.clone();
972        let grouped = query.group_by.contains(&c);
973        let text = if grouped { &labels.grid_ungroup_column } else { &labels.grid_group_by_column };
974        entries.push(Entry::Item(
975            item(text, IconName::Layers).on_select(move || q.update(|q| q.toggle_group(c))),
976        ));
977    }
978    let arrange = s.arrangement;
979    let pinned = arrange.pinned.get(c).copied().unwrap_or(false);
980    let a = h.arrange.clone();
981    entries.push(Entry::Item(
982        item(
983            if pinned { &labels.grid_unpin_column } else { &labels.grid_pin_column },
984            if pinned { IconName::PinOff } else { IconName::Pin },
985        )
986        .on_select(move || a.update(|a| a.pinned[c] = !pinned)),
987    ));
988    if s.grid.remote.is_none() {
989        let edit = h.summary_edit.clone();
990        let (draft, start) = (h.summary_draft.clone(), first_draft(s, c));
991        entries.push(Entry::Item(item(&labels.grid_edit_summary, IconName::Sigma).on_select(move || {
992            draft.set(start.clone());
993            edit.set(Some(c));
994        })));
995    }
996    if s.grid.columns[c].resizable {
997        let (widths, width) = (h.widths.clone(), fit_width(s, c));
998        entries.push(Entry::Item(
999            item(&labels.grid_fit_column, IconName::Columns3)
1000                .on_select(move || widths.update(|ws| ws[c] = width)),
1001        ));
1002    }
1003    let q = h.query.clone();
1004    entries.push(Entry::Item(item(&labels.grid_hide_column, IconName::EyeOff).on_select(move || {
1005        q.update(|q| {
1006            if q.hidden.len() <= c {
1007                q.hidden.resize(c + 1, false);
1008            }
1009            q.hidden[c] = true;
1010        });
1011    })));
1012    if !query.filter(c).trim().is_empty() {
1013        let q = h.query.clone();
1014        entries.push(Entry::Item(
1015            item(&labels.grid_clear_filter, IconName::Filter)
1016                .on_select(move || q.update(|q| q.set_filter(c, String::new()))),
1017        ));
1018    }
1019    if s.grid.remote.is_none() {
1020        entries.push(Entry::Separator);
1021        let a = h.arrange.clone();
1022        let showing = arrange.group_panel;
1023        entries.push(Entry::Item(
1024            item(if showing { &labels.grid_hide_panel } else { &labels.grid_show_panel }, IconName::Rows3)
1025                .on_select(move || a.update(|a| a.group_panel = !showing)),
1026        ));
1027    }
1028    entries
1029}
1030
1031/// What a formula's value looks like in its column.
1032fn formula_display(col: &GridColumn, value: &ravenrs_criteria::Value) -> String {
1033    use ravenrs_criteria::Value as Field;
1034    match value {
1035        Field::Number(n) => col.display_summary(Summary::Sum, *n),
1036        Field::Text(text) => text.clone(),
1037        Field::Bool(b) => b.to_string(),
1038        Field::Null => "โ€”".to_owned(),
1039        other => format!("{other:?}"),
1040    }
1041}
1042
1043/// What the formula field starts with.
1044fn first_draft(s: &Shared<'_>, c: usize) -> String {
1045    s.arrangement.formulas.get(c).cloned().flatten().unwrap_or_else(|| {
1046        let title = &s.grid.columns[c].title;
1047        match s.arrangement.summaries.get(c).copied().flatten() {
1048            Some(Summary::Count) => "Count()".to_owned(),
1049            Some(Summary::Average) => format!("Avg([{title}])"),
1050            Some(Summary::Min) => format!("Min([{title}])"),
1051            Some(Summary::Max) => format!("Max([{title}])"),
1052            _ => format!("Sum([{title}])"),
1053        }
1054    })
1055}
1056
1057/// The kinds of summary to choose from, in the order the editor lists them.
1058const SUMMARY_KINDS: [Option<Summary>; 6] = [
1059    None,
1060    Some(Summary::Sum),
1061    Some(Summary::Average),
1062    Some(Summary::Min),
1063    Some(Summary::Max),
1064    Some(Summary::Count),
1065];
1066const FORMULA_CHOICE: usize = SUMMARY_KINDS.len();
1067const FORMULA_FUNCTIONS: [&str; 6] = ["Sum(", "Avg(", "Min(", "Max(", "Count()", "Iif("];
1068
1069/// Edits what a column sums up: one of the usual kinds, or a formula.
1070struct SummaryEditor {
1071    column: usize,
1072    columns: Vec<GridColumn>,
1073    rows: Rc<Vec<Row>>,
1074    matched: Rc<QueryResult>,
1075    arrange: State<Arrangement>,
1076    draft: State<String>,
1077    close: Rc<dyn Fn()>,
1078}
1079
1080impl View for SummaryEditor {
1081    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1082        let theme = *cx.theme();
1083        let labels = cx.labels();
1084        let c = self.column;
1085        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
1086        let arrangement = self.arrange.get();
1087        let by_formula = arrangement.formulas[c].is_some();
1088        let title = format!("{} {}", labels.grid_summary_of, self.columns[c].title);
1089        let mut lines = vec![util::label(cx, &title, TextStyle::Callout, Some(Weight::SEMIBOLD), ink)];
1090
1091        let mut names: Vec<String> = SUMMARY_KINDS
1092            .iter()
1093            .map(|kind| kind.map_or(labels.grid_summary_none.clone(), |s| labels.summary(s).to_owned()))
1094            .collect();
1095        names.push(labels.grid_summary_formula.clone());
1096        let (read, write, draft) = (self.arrange.clone(), self.arrange.clone(), self.draft.clone());
1097        let choice = Binding::new(
1098            move || {
1099                let now = read.get();
1100                if now.formulas[c].is_some() {
1101                    return FORMULA_CHOICE;
1102                }
1103                SUMMARY_KINDS.iter().position(|k| *k == now.summaries[c]).unwrap_or(0)
1104            },
1105            move |i: usize| {
1106                let formula = (i == FORMULA_CHOICE).then(|| draft.get());
1107                write.update(|a| {
1108                    a.formulas[c].clone_from(&formula);
1109                    if let Some(kind) = SUMMARY_KINDS.get(i) {
1110                        a.summaries[c] = *kind;
1111                    }
1112                });
1113            },
1114        );
1115        lines.push(cx.child(&crate::SegmentedControl::new(choice, names).label(title.clone())));
1116
1117        if by_formula {
1118            let text = self.draft.get();
1119            let checked = check_formula(&self.columns, &text)
1120                .and_then(|parsed| formula_value(&self.columns, &self.rows, &self.matched.matched, &parsed));
1121            let (write, draft, close) = (self.arrange.clone(), self.draft.clone(), self.close.clone());
1122            let valid = checked.is_ok();
1123            let apply = Rc::new(move || {
1124                if valid {
1125                    let formula = draft.get();
1126                    write.update(|a| a.formulas[c] = Some(formula.clone()));
1127                    close();
1128                }
1129            });
1130            let on_enter = apply.clone();
1131            lines.push(
1132                cx.child(
1133                    &TextField::new(self.draft.binding())
1134                        .placeholder(labels.grid_formula_hint.clone())
1135                        .autofocus()
1136                        .on_submit(move |_| on_enter()),
1137                ),
1138            );
1139            lines.push(match &checked {
1140                Ok(value) => {
1141                    let shown = format!(
1142                        "{}: {}",
1143                        labels.grid_formula_result,
1144                        formula_display(&self.columns[c], value)
1145                    );
1146                    util::label(cx, &shown, TextStyle::Callout, Some(Weight::MEDIUM), ink)
1147                }
1148                Err(problem) => {
1149                    let shown = format!("{}: {problem}", labels.grid_formula_error);
1150                    cx.child(&crate::Text::new(shown).style(TextStyle::Callout).foreground(Semantic::Danger))
1151                }
1152            });
1153            let inserter = |piece: String, name: String, draft: State<String>| {
1154                Button::new(name).style(ButtonStyle::Secondary).size(ButtonSize::Small).on_click(move || {
1155                    draft.update(|text| text.push_str(&piece));
1156                })
1157            };
1158            lines.push(util::label(cx, &labels.grid_formula_columns, TextStyle::Caption, None, muted));
1159            let chips: Vec<NodeId> = self
1160                .columns
1161                .iter()
1162                .enumerate()
1163                .map(|(i, col)| {
1164                    let piece = format!("[{}]", col.title);
1165                    cx.keyed_child(i as u64, &inserter(piece, col.title.clone(), self.draft.clone()))
1166                })
1167                .collect();
1168            lines.push(cx.container(
1169                LayoutStyle { spacing: 4.0, wrap: true, ..LayoutStyle::stack(Axis::Horizontal) },
1170                &chips,
1171                Paint::None,
1172            ));
1173            lines.push(util::label(cx, &labels.grid_formula_functions, TextStyle::Caption, None, muted));
1174            let chips: Vec<NodeId> = FORMULA_FUNCTIONS
1175                .iter()
1176                .enumerate()
1177                .map(|(i, f)| {
1178                    cx.keyed_child(
1179                        100 + i as u64,
1180                        &inserter((*f).to_owned(), (*f).to_owned(), self.draft.clone()),
1181                    )
1182                })
1183                .collect();
1184            lines.push(cx.container(
1185                LayoutStyle { spacing: 4.0, wrap: true, ..LayoutStyle::stack(Axis::Horizontal) },
1186                &chips,
1187                Paint::None,
1188            ));
1189            lines.push(
1190                cx.child(
1191                    &Button::new(labels.grid_apply.clone())
1192                        .disabled(!valid)
1193                        .size(ButtonSize::Small)
1194                        .on_click(move || apply()),
1195                ),
1196            );
1197        }
1198        cx.container(
1199            LayoutStyle {
1200                spacing: 10.0,
1201                width: Some(SUMMARY_EDITOR_WIDTH),
1202                align: Align::Stretch,
1203                ..LayoutStyle::stack(Axis::Vertical)
1204            },
1205            &lines,
1206            Paint::None,
1207        )
1208    }
1209}
1210
1211/// A column's total at the bottom: click to change what it sums up.
1212struct FooterCell<'a> {
1213    column: usize,
1214    shared: &'a Shared<'a>,
1215}
1216
1217impl View for FooterCell<'_> {
1218    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1219        let theme = *cx.theme();
1220        let (s, c) = (self.shared, self.column);
1221        let col = &s.grid.columns[c];
1222        let labels = cx.labels();
1223        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
1224        let bold = util::attrs(&theme, TextStyle::Callout, Some(Weight::SEMIBOLD));
1225        let hovered = cx.is_hover_within();
1226        let editable = s.grid.remote.is_none();
1227        let figure = match s.formula_text(c, (0, s.result.matched.len())) {
1228            Some(text) => Some((text, labels.grid_summary_formula.clone())),
1229            None => s.arrangement.summaries[c]
1230                .zip(s.result.totals.get(c).copied().flatten())
1231                .map(|(summary, v)| (col.display_summary(summary, v), labels.summary(summary).to_owned())),
1232        };
1233        let mut parts = Vec::new();
1234        let by_formula = s.arrangement.formulas[c].is_some();
1235        match &figure {
1236            Some((text, kind)) => {
1237                if by_formula {
1238                    parts.push(util::icon(cx, IconName::Sigma, 13.0, theme.color(Semantic::Accent)));
1239                } else if s.widths[c] >= FOOTER_NAME_FROM {
1240                    parts.push(util::label(cx, kind, TextStyle::Caption, None, muted));
1241                }
1242                parts.push(cx.text(text, &bold, ink, LayoutStyle::leaf()));
1243            }
1244            None if hovered && editable => parts.push(util::icon(cx, IconName::Sigma, 14.0, muted)),
1245            None => {}
1246        }
1247        let fixed = s.fixed_x(c);
1248        let mut style = cell_style(s.widths[c], FOOTER_HEIGHT, col.align);
1249        // In line with the column's values, even when the total barely fits.
1250        style.padding = Insets { left: 2.0, right: 12.0, ..Insets::ZERO };
1251        style.absolute = fixed.map(|x| (x, 0.0));
1252        let surface = theme.color(Semantic::Surface);
1253        let band = theme.color(Semantic::Control).faded(0.6);
1254        let tint = if hovered && editable { ink.with_alpha(0.04) } else { Color::TRANSPARENT };
1255        let solid = fixed.is_some();
1256        let node = cx.container(
1257            style,
1258            &parts,
1259            Paint::Custom(Rc::new(move |f: Rect, scene| {
1260                if solid {
1261                    scene.push_quad(Quad::new(f, surface));
1262                    scene.push_quad(Quad::new(f, band));
1263                }
1264                scene.push_quad(Quad::new(f, tint));
1265            })),
1266        );
1267        if fixed.is_some() {
1268            cx.translate(node, Point::new(s.offset_x, 0.0));
1269        }
1270        if editable {
1271            let edit = s.h.summary_edit.clone();
1272            let (draft, start) = (s.h.summary_draft.clone(), first_draft(s, c));
1273            cx.on_click(
1274                node,
1275                Rc::new(move || {
1276                    draft.set(start.clone());
1277                    edit.set(Some(c));
1278                }),
1279            );
1280            cx.cursor(node, CursorIcon::Pointer);
1281            let value = figure.map_or_else(String::new, |(text, _)| text);
1282            cx.semantics(
1283                node,
1284                Semantics::new(Role::Button)
1285                    .name(format!("{} {}", labels.grid_edit_summary, col.title))
1286                    .value(value),
1287            );
1288        }
1289        node
1290    }
1291}
1292
1293/// Selection by the row indices the grid works with, kept as the keys of those rows.
1294fn keyed_selection(
1295    keys: Binding<BTreeSet<String>>,
1296    column: usize,
1297    data: GridData,
1298    remote: Option<RemoteRows>,
1299) -> Binding<BTreeSet<usize>> {
1300    let held = move || -> Vec<(usize, Row)> {
1301        match &remote {
1302            Some(remote) => remote.held_rows(),
1303            None => data.rows().iter().cloned().enumerate().collect(),
1304        }
1305    };
1306    let key_of = move |row: &Row| row.get(column).map(Value::key_text).unwrap_or_default();
1307    let (read, write, rows_read, rows_write) = (keys.clone(), keys, held.clone(), held);
1308    Binding::new(
1309        move || {
1310            let wanted = read.get();
1311            if wanted.is_empty() {
1312                return BTreeSet::new();
1313            }
1314            rows_read().iter().filter(|(_, row)| wanted.contains(&key_of(row))).map(|(i, _)| *i).collect()
1315        },
1316        move |picked: BTreeSet<usize>| {
1317            let rows = rows_write();
1318            // A record out of reach, on a server, stays selected until someone changes it.
1319            let at_hand: BTreeSet<String> = rows.iter().map(|(_, row)| key_of(row)).collect();
1320            let mut next: BTreeSet<String> =
1321                write.get().into_iter().filter(|key| !at_hand.contains(key)).collect();
1322            next.extend(rows.iter().filter(|(i, _)| picked.contains(i)).map(|(_, row)| key_of(row)));
1323            write.set(next);
1324        },
1325    )
1326}
1327
1328/// A value as the filter expression language writes it.
1329fn value_literal(value: &Value) -> Option<String> {
1330    match value {
1331        Value::Text(text) => Some(format!("'{}'", text.replace('\'', "''"))),
1332        Value::Number(n) => Some(n.to_string()),
1333        Value::Bool(b) => Some(if *b { "True" } else { "False" }.to_owned()),
1334        Value::Date(date) => Some(format!("#{date}#")),
1335        _ => None,
1336    }
1337}
1338
1339/// The distinct values of a column, with what the filter menu shows for each.
1340fn values_of(rows: &[Row], col: &GridColumn, c: usize) -> Vec<(String, Value)> {
1341    let mut seen = BTreeMap::new();
1342    for row in rows {
1343        let Some(value) = row.get(c) else { continue };
1344        if matches!(value, Value::Empty) {
1345            continue;
1346        }
1347        let shown = col.display_cow(value);
1348        if !seen.contains_key(shown.as_ref()) {
1349            seen.insert(shown.into_owned(), value.clone());
1350            if seen.len() >= MAX_FILTER_VALUES {
1351                break;
1352            }
1353        }
1354    }
1355    seen.into_iter().collect()
1356}
1357
1358/// A column's filter by value: a list of what is in it, to check.
1359struct ValueFilter {
1360    title: String,
1361    column: usize,
1362    values: Vec<(String, Value)>,
1363    query: State<GridQuery>,
1364    chosen: State<BTreeSet<String>>,
1365    search: State<String>,
1366}
1367
1368#[cfg_attr(feature = "live", ravenrs_live::expose)]
1369impl ValueFilter {
1370    fn write(&self, chosen: &BTreeSet<String>) {
1371        let literals: Vec<String> = self
1372            .values
1373            .iter()
1374            .filter(|(shown, _)| chosen.contains(shown))
1375            .filter_map(|(_, value)| value_literal(value))
1376            .collect();
1377        let text = if literals.is_empty() {
1378            String::new()
1379        } else {
1380            format!("[{}] In ({})", self.title, literals.join(", "))
1381        };
1382        let c = self.column;
1383        self.query.update(|q| q.set_filter(c, text));
1384    }
1385}
1386
1387impl View for ValueFilter {
1388    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1389        let labels = cx.labels();
1390        let chosen = self.chosen.get();
1391        let needle = self.search.get().to_lowercase();
1392        let mut lines = vec![
1393            cx.child(
1394                &TextField::new(self.search.binding())
1395                    .placeholder(labels.grid_search.clone())
1396                    .icon(IconName::Search)
1397                    .compact(),
1398            ),
1399        ];
1400        let mut list = Vec::new();
1401        for (shown, _) in self.values.iter().filter(|(shown, _)| shown.to_lowercase().contains(&needle)) {
1402            let on = chosen.contains(shown);
1403            let (name, this) = (shown.clone(), ValueFilterHandle::of(self));
1404            let binding = Binding::new(move || on, move |checked: bool| this.set(&name, checked));
1405            list.push(cx.keyed_child(hash_of(shown), &crate::Checkbox::new(shown.clone(), binding)));
1406        }
1407        let inner = cx.container(
1408            LayoutStyle { spacing: 6.0, ..LayoutStyle::stack(Axis::Vertical) },
1409            &list,
1410            Paint::None,
1411        );
1412        let scroller = cx.container(
1413            LayoutStyle {
1414                height: Some((list.len() as f32 * 28.0).min(260.0)),
1415                overflow_hidden: true,
1416                align_self: Some(Align::Stretch),
1417                ..LayoutStyle::stack(Axis::Vertical)
1418            },
1419            &[inner],
1420            Paint::None,
1421        );
1422        cx.scrollable(scroller);
1423        lines.push(scroller);
1424        if !chosen.is_empty() {
1425            let this = ValueFilterHandle::of(self);
1426            lines.push(
1427                cx.child(
1428                    &Button::new(labels.grid_clear_filter.clone())
1429                        .style(ButtonStyle::Ghost)
1430                        .size(ButtonSize::Small)
1431                        .icon(IconName::X)
1432                        .on_click(move || this.clear()),
1433                ),
1434            );
1435        }
1436        cx.container(
1437            LayoutStyle {
1438                spacing: 8.0,
1439                width: Some(240.0),
1440                align: Align::Stretch,
1441                ..LayoutStyle::stack(Axis::Vertical)
1442            },
1443            &lines,
1444            Paint::None,
1445        )
1446    }
1447}
1448
1449/// What a checkbox of a [`ValueFilter`] keeps to write the filter.
1450#[derive(Clone)]
1451struct ValueFilterHandle {
1452    title: String,
1453    column: usize,
1454    values: Rc<Vec<(String, Value)>>,
1455    query: State<GridQuery>,
1456    chosen: State<BTreeSet<String>>,
1457}
1458
1459#[cfg_attr(feature = "live", ravenrs_live::expose)]
1460impl ValueFilterHandle {
1461    fn of(filter: &ValueFilter) -> Self {
1462        Self {
1463            title: filter.title.clone(),
1464            column: filter.column,
1465            values: Rc::new(filter.values.clone()),
1466            query: filter.query.clone(),
1467            chosen: filter.chosen.clone(),
1468        }
1469    }
1470
1471    fn write(&self, chosen: &BTreeSet<String>) {
1472        ValueFilter {
1473            title: self.title.clone(),
1474            column: self.column,
1475            values: self.values.as_ref().clone(),
1476            query: self.query.clone(),
1477            chosen: self.chosen.clone(),
1478            search: State::new(String::new()),
1479        }
1480        .write(chosen);
1481    }
1482
1483    fn set(&self, name: &str, on: bool) {
1484        let mut chosen = self.chosen.get();
1485        if on {
1486            chosen.insert(name.to_owned());
1487        } else {
1488            chosen.remove(name);
1489        }
1490        self.write(&chosen);
1491        self.chosen.set(chosen);
1492    }
1493
1494    fn clear(&self) {
1495        self.write(&BTreeSet::new());
1496        self.chosen.set(BTreeSet::new());
1497    }
1498}
1499
1500fn hash_of(text: &str) -> u64 {
1501    text.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |h, b| (h ^ u64::from(b)).wrapping_mul(0x100_0000_01b3))
1502}
1503
1504struct HeaderCell<'a> {
1505    column: usize,
1506    shared: &'a Shared<'a>,
1507}
1508
1509impl View for HeaderCell<'_> {
1510    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1511        let theme = *cx.theme();
1512        let s = self.shared;
1513        let c = self.column;
1514        let col = &s.grid.columns[c];
1515        let labels = cx.labels();
1516        let query = s.h.query.peek().clone();
1517        let sort = query.sort.iter().position(|(sc, _)| *sc == c).map(|i| (i, query.sort[i].1));
1518        let hovered = cx.is_hover_within();
1519        let accent = theme.color(Semantic::Accent);
1520        let ink = if sort.is_some() || hovered {
1521            theme.color(Semantic::Ink)
1522        } else {
1523            theme.color(Semantic::InkMuted)
1524        };
1525        let a = util::attrs(&theme, TextStyle::Caption, Some(Weight::SEMIBOLD));
1526        let mut content = Vec::new();
1527        if s.arrangement.pinned.get(c).copied().unwrap_or(false) {
1528            content.push(util::icon(cx, IconName::Pin, 12.0, theme.color(Semantic::InkMuted)));
1529        }
1530        content.push(cx.text(&col.title, &a, ink, LayoutStyle::leaf()));
1531        if let Some((priority, dir)) = sort {
1532            let icon = if dir == SortDirection::Ascending { IconName::ArrowUp } else { IconName::ArrowDown };
1533            content.push(util::icon(cx, icon, 13.0, accent));
1534            if query.sort.len() > 1 {
1535                content.push(util::label(cx, &(priority + 1).to_string(), TextStyle::Caption, None, ink));
1536            }
1537        }
1538        let width = s.widths[c];
1539        let filtered = !query.filter(c).trim().is_empty();
1540        let can_filter = col.filterable && s.grid.remote.is_none();
1541        let opened_filter = s.h.filter_open.get() == Some(c);
1542        let opened_menu = s.h.menu.get().is_some_and(|(column, _)| column == c);
1543        let tools = hovered || opened_filter || opened_menu;
1544        let mut label_style = LayoutStyle {
1545            grow: 1.0,
1546            min_width: Some(0.0),
1547            height: Some(HEADER_HEIGHT),
1548            spacing: 6.0,
1549            align: Align::Center,
1550            justify: justify(col.align),
1551            overflow_hidden: true,
1552            ..LayoutStyle::stack(Axis::Horizontal)
1553        };
1554        label_style.padding = Insets { left: 12.0, right: 4.0, ..Insets::ZERO };
1555        let label = cx.container(label_style, &content, Paint::None);
1556        let mut semantics = Semantics::new(Role::ColumnHeader).name(col.title.clone());
1557        if let Some((_, dir)) = sort {
1558            semantics = semantics.description(if dir == SortDirection::Ascending {
1559                labels.grid_sorted_ascending.clone()
1560            } else {
1561                labels.grid_sorted_descending.clone()
1562            });
1563        }
1564        cx.semantics(label, semantics);
1565        let dragged = s.h.dragged.clone();
1566        if col.sortable {
1567            let q = s.h.query.clone();
1568            let m = s.h.modifiers.clone();
1569            cx.on_click(
1570                label,
1571                Rc::new(move || {
1572                    // Letting go of a drag on its own header is not a click.
1573                    if dragged.replace(false) {
1574                        return;
1575                    }
1576                    let mods = m.get();
1577                    q.update(|q| q.toggle_sort(c, mods.shift || mods.ctrl || mods.meta));
1578                }),
1579            );
1580            cx.cursor(label, CursorIcon::Pointer);
1581        }
1582        cx.on_drag(label, drag_handler(&s.h, &s.column_facts, DragSource::Column(c)));
1583        let menu = s.h.menu.clone();
1584        cx.on_context_menu(label, Rc::new(move |p| menu.set(Some((c, Some(p))))));
1585        let mut parts = vec![label];
1586        let mut funnel_node = None;
1587        if can_filter && (tools || filtered) {
1588            let opener = s.h.filter_open.clone();
1589            let chosen = s.h.filter_chosen.clone();
1590            let search = s.h.filter_search.clone();
1591            let starts = if opened_filter { None } else { Some(c) };
1592            let (q2, rows2, col2) = (s.h.query.clone(), s.rows_rc.clone(), col.clone());
1593            let node = cx.keyed_child(
1594                1,
1595                &crate::IconButton::new(IconName::Filter)
1596                    .label(format!("{} {}", labels.grid_filter_values, col.title))
1597                    .style(if filtered { ButtonStyle::Secondary } else { ButtonStyle::Ghost })
1598                    .size(ButtonSize::Small)
1599                    .on_click(move || {
1600                        // The list starts from what the filter holds, written by hand or not.
1601                        let text = q2.peek().filter(c).to_owned();
1602                        let values = values_of(&rows2, &col2, c);
1603                        chosen.set(
1604                            values
1605                                .into_iter()
1606                                .filter(|(_, v)| {
1607                                    value_literal(v).is_some_and(|l| {
1608                                        text.contains(&format!("{l},")) || text.contains(&format!("{l})"))
1609                                    })
1610                                })
1611                                .map(|(shown, _)| shown)
1612                                .collect(),
1613                        );
1614                        search.set(String::new());
1615                        opener.set(starts);
1616                    }),
1617            );
1618            funnel_node = Some(node);
1619            parts.push(node);
1620        }
1621        let mut menu_node = None;
1622        if tools {
1623            let opener = s.h.menu.clone();
1624            let node = cx.keyed_child(
1625                2,
1626                &crate::IconButton::new(IconName::EllipsisVertical)
1627                    .label(format!("{} {}", labels.grid_column_menu, col.title))
1628                    .style(ButtonStyle::Ghost)
1629                    .size(ButtonSize::Small)
1630                    .on_click(move || {
1631                        opener.update(|m| *m = if m.is_some() { None } else { Some((c, None)) });
1632                    }),
1633            );
1634            menu_node = Some(node);
1635            parts.push(node);
1636        }
1637        if col.resizable {
1638            parts.push(cx.keyed_child(3, &ResizeHandle { column: c, shared: s }));
1639        }
1640
1641        let fixed = s.fixed_x(c);
1642        let lit =
1643            s.drop_before == Some(Some(c)) || (s.drop_before == Some(None) && s.visible.last() == Some(&c));
1644        let at_end = s.drop_before == Some(None);
1645        let zones = s.h.zones.clone();
1646        let hover_fill =
1647            if hovered { theme.color(Semantic::Ink).with_alpha(0.03) } else { Color::TRANSPARENT };
1648        let solid = fixed.map(|_| theme.color(Semantic::Surface));
1649        let band = theme.color(Semantic::Control).faded(0.6);
1650        let edge = (s.last_fixed == Some(c)).then(|| theme.color(Semantic::Border));
1651        let node = cx.container(
1652            LayoutStyle {
1653                width: Some(width),
1654                height: Some(HEADER_HEIGHT),
1655                align: Align::Center,
1656                absolute: fixed.map(|x| (x, 0.0)),
1657                ..LayoutStyle::stack(Axis::Horizontal)
1658            },
1659            &parts,
1660            Paint::Custom(Rc::new(move |f: Rect, scene| {
1661                zones.borrow_mut().cells.insert(c, f);
1662                if let Some(solid) = solid {
1663                    scene.push_quad(Quad::new(f, solid));
1664                    scene.push_quad(Quad::new(f, band));
1665                }
1666                scene.push_quad(Quad::new(f, hover_fill));
1667                if let Some(edge) = edge {
1668                    scene.push_quad(Quad::new(Rect::new(f.max_x() - 1.0, f.y(), 1.0, f.height()), edge));
1669                }
1670                if lit {
1671                    let x = if at_end { f.max_x() - 2.0 } else { f.x() };
1672                    scene.push_quad(Quad::new(Rect::new(x, f.y() + 4.0, 2.0, f.height() - 8.0), accent));
1673                }
1674            })),
1675        );
1676        if fixed.is_some() {
1677            cx.translate(node, Point::new(s.offset_x, 0.0));
1678        }
1679        let opened = *s.h.menu.peek();
1680        if opened_menu && let Some(button_or_point) = opened {
1681            let entries = column_menu(s, c, &labels);
1682            let menu = s.h.menu.clone();
1683            let close: Rc<dyn Fn()> = Rc::new(move || menu.set(None));
1684            let list = MenuList { entries: &entries, close: close.clone(), on_side: None };
1685            match button_or_point.1 {
1686                Some(p) => cx.overlay(node, Placement::At(p), &list, Some(close)),
1687                None => cx.overlay(menu_node.unwrap_or(node), Placement::Below, &list, Some(close)),
1688            }
1689        }
1690        if opened_filter {
1691            let close: Rc<dyn Fn()> = {
1692                let open = s.h.filter_open.clone();
1693                Rc::new(move || open.set(None))
1694            };
1695            let filter = ValueFilter {
1696                title: col.title.clone(),
1697                column: c,
1698                values: values_of(s.rows, col, c),
1699                query: s.h.query.clone(),
1700                chosen: s.h.filter_chosen.clone(),
1701                search: s.h.filter_search.clone(),
1702            };
1703            cx.overlay(funnel_node.unwrap_or(node), Placement::Below, &PanelOf(&filter), Some(close));
1704        }
1705        node
1706    }
1707}
1708
1709struct FilterCell<'a> {
1710    column: usize,
1711    shared: &'a Shared<'a>,
1712}
1713
1714impl View for FilterCell<'_> {
1715    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1716        let theme = *cx.theme();
1717        let s = self.shared;
1718        let c = self.column;
1719        let col = &s.grid.columns[c];
1720        let mut children = Vec::new();
1721        if col.filterable {
1722            let (read, write) = (s.h.query.clone(), s.h.query.clone());
1723            let binding = Binding::new(
1724                move || read.with(|q| q.filter(c).to_owned()),
1725                move |t| write.update(|q| q.set_filter(c, t)),
1726            );
1727            let labels = cx.labels();
1728            let hint =
1729                if col.number.is_some() { &labels.grid_number_filter_hint } else { &labels.grid_filter_hint };
1730            let mut field = TextField::new(binding).placeholder(hint.clone()).compact().flat();
1731            if matches!(col.align, CellAlign::End) {
1732                field = field.end_aligned();
1733            }
1734            let field = cx.child(&field);
1735            cx.label_control(field, &format!("{} {}", labels.grid_filter_column, col.title), Some(hint));
1736            children.push(field);
1737        }
1738        let mut style = cell_style(s.widths[c], HEADER_HEIGHT, CellAlign::Start);
1739        // Its text starts where the column's does: the field brings the same room as a cell.
1740        style.padding = Insets::ZERO;
1741        style.align = Align::Stretch;
1742        let fixed = s.fixed_x(c);
1743        style.absolute = fixed.map(|x| (x, 0.0));
1744        let paint =
1745            fixed.map_or(Paint::None, |_| Paint::Quad(QuadPaint::new(theme.color(Semantic::Surface))));
1746        let node = cx.container(LayoutStyle { align: Align::Stretch, ..style }, &children, paint);
1747        if fixed.is_some() {
1748            cx.translate(node, Point::new(s.offset_x, 0.0));
1749        }
1750        node
1751    }
1752}
1753
1754/// A chip of the group panel: drag to reorder, or off the panel to ungroup; click to
1755/// change the direction of the groups; arrows with Ctrl move it, Delete removes it.
1756struct GroupChip<'a> {
1757    index: usize,
1758    column: usize,
1759    grid: &'a DataGrid,
1760    h: &'a Handles,
1761    facts: &'a Rc<Vec<(String, bool)>>,
1762}
1763
1764impl View for GroupChip<'_> {
1765    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1766        let theme = *cx.theme();
1767        let labels = cx.labels();
1768        let (c, i, h) = (self.column, self.index, self.h);
1769        let title = self.grid.columns.get(c).map_or("?", |col| col.title.as_str());
1770        let query = h.query.peek().clone();
1771        let descending = query.sort.iter().any(|(s, d)| *s == c && *d == SortDirection::Descending);
1772        let (ink, muted, accent) =
1773            (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted), theme.color(Semantic::Accent));
1774        let grip = util::icon(cx, IconName::EllipsisVertical, 12.0, muted);
1775        let name = util::label(cx, title, TextStyle::Callout, Some(Weight::MEDIUM), ink);
1776        let arrow =
1777            util::icon(cx, if descending { IconName::ArrowDown } else { IconName::ArrowUp }, 13.0, accent);
1778        let body = cx.container(
1779            LayoutStyle {
1780                spacing: 6.0,
1781                align: Align::Center,
1782                height: Some(28.0),
1783                padding: Insets { left: 8.0, right: 2.0, ..Insets::ZERO },
1784                ..LayoutStyle::stack(Axis::Horizontal)
1785            },
1786            &[grip, name, arrow],
1787            Paint::None,
1788        );
1789        cx.semantics(body, Semantics::new(Role::Button).name(format!("{} {title}", labels.grid_grouped_by)));
1790        cx.cursor(body, CursorIcon::Pointer);
1791        let (q, dragged) = (h.query.clone(), h.dragged.clone());
1792        cx.on_click(
1793            body,
1794            Rc::new(move || {
1795                if dragged.replace(false) {
1796                    return;
1797                }
1798                q.update(|q| {
1799                    q.sort.retain(|(s, _)| *s != c);
1800                    if !descending {
1801                        q.sort.push((c, SortDirection::Descending));
1802                    }
1803                });
1804            }),
1805        );
1806        cx.on_drag(body, drag_handler(h, self.facts, DragSource::Chip(c)));
1807        cx.focusable(body, 6.0);
1808        let q = h.query.clone();
1809        cx.on_key(
1810            body,
1811            Rc::new(move |key, mods| {
1812                match key {
1813                    Key::ArrowLeft if (mods.ctrl || mods.meta) && i > 0 => q.update(|q| {
1814                        q.group_by.swap(i, i - 1);
1815                        q.collapsed.clear();
1816                    }),
1817                    Key::ArrowRight if mods.ctrl || mods.meta => q.update(|q| {
1818                        if i + 1 < q.group_by.len() {
1819                            q.group_by.swap(i, i + 1);
1820                            q.collapsed.clear();
1821                        }
1822                    }),
1823                    Key::Delete | Key::Backspace => q.update(|q| q.toggle_group(c)),
1824                    _ => return false,
1825                }
1826                true
1827            }),
1828        );
1829        let q = h.query.clone();
1830        let remove = cx.keyed_child(
1831            1,
1832            &crate::IconButton::new(IconName::X)
1833                .label(format!("{} {title}", labels.grid_remove_group))
1834                .style(ButtonStyle::Ghost)
1835                .size(ButtonSize::Small)
1836                .on_click(move || q.update(|q| q.toggle_group(c))),
1837        );
1838        let zones = h.zones.clone();
1839        let (fill, line) = (theme.color(Semantic::SurfaceElevated), theme.color(Semantic::Border));
1840        let radius = theme.radii.sm;
1841        let moving = h.drag.peek().is_some_and(|d| d.source == DragSource::Chip(c));
1842        let (fill, line) = if moving { (fill.faded(0.4), line.faded(0.4)) } else { (fill, line) };
1843        cx.container(
1844            LayoutStyle { align: Align::Center, height: Some(30.0), ..LayoutStyle::stack(Axis::Horizontal) },
1845            &[body, remove],
1846            Paint::Custom(Rc::new(move |f: Rect, scene| {
1847                zones.borrow_mut().chips.insert(i, f);
1848                scene.push_quad(Quad::new(f, fill).radius(radius).border(1.0, line));
1849            })),
1850        )
1851    }
1852}
1853
1854/// What follows the pointer during a drag.
1855struct DragGhost {
1856    title: String,
1857    grouping: bool,
1858    ungrouping: bool,
1859}
1860
1861impl View for DragGhost {
1862    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1863        let theme = *cx.theme();
1864        let accent = theme.color(Semantic::Accent);
1865        let icon = if self.ungrouping {
1866            IconName::X
1867        } else if self.grouping {
1868            IconName::Layers
1869        } else {
1870            IconName::Columns3
1871        };
1872        let mark = util::icon(cx, icon, 14.0, accent);
1873        let name = util::label(
1874            cx,
1875            &self.title,
1876            TextStyle::Callout,
1877            Some(Weight::SEMIBOLD),
1878            theme.color(Semantic::Ink),
1879        );
1880        let node = cx.container(
1881            LayoutStyle {
1882                spacing: 6.0,
1883                align: Align::Center,
1884                padding: Insets::symmetric(6.0, 10.0),
1885                ..LayoutStyle::stack(Axis::Horizontal)
1886            },
1887            &[mark, name],
1888            Paint::Quad(QuadPaint {
1889                corner_radius: theme.radii.sm,
1890                border: Some(ravenrs_core::Border { width: 1.0, color: accent }),
1891                shadow: Some(theme.elevation.raised),
1892                ..QuadPaint::new(theme.color(Semantic::SurfaceElevated))
1893            }),
1894        );
1895        cx.semantics(node, Semantics::hidden());
1896        node
1897    }
1898}
1899
1900/// A row that has not arrived from the server yet.
1901struct WaitingRow<'a> {
1902    shared: &'a Shared<'a>,
1903}
1904
1905impl View for WaitingRow<'_> {
1906    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1907        let theme = *cx.theme();
1908        let s = self.shared;
1909        let bar = theme.color(Semantic::Control);
1910        let line = theme.color(Semantic::Border).faded(0.6);
1911        let starts: Vec<(f32, f32)> = {
1912            let mut x = s.select_width;
1913            s.visible
1914                .iter()
1915                .map(|&c| {
1916                    let cell = (x, s.widths[c]);
1917                    x += s.widths[c];
1918                    cell
1919                })
1920                .collect()
1921        };
1922        let node = cx.container(
1923            LayoutStyle {
1924                width: Some(s.total_width),
1925                height: Some(s.grid.row_height),
1926                ..LayoutStyle::leaf()
1927            },
1928            &[],
1929            Paint::Custom(Rc::new(move |f: Rect, scene| {
1930                for (x, width) in &starts {
1931                    let inner =
1932                        Rect::new(f.x() + x + 12.0, f.center().y - 5.0, (width - 40.0).max(8.0), 10.0);
1933                    scene.push_quad(Quad::new(inner, bar).radius(5.0));
1934                }
1935                scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
1936            })),
1937        );
1938        cx.semantics(node, Semantics::hidden());
1939        node
1940    }
1941}
1942
1943struct DataRow<'a> {
1944    index: usize,
1945    /// Second, fourth and so on since the last group row.
1946    even: bool,
1947    row: usize,
1948    values: Row,
1949    shared: &'a Shared<'a>,
1950}
1951
1952impl View for DataRow<'_> {
1953    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1954        let theme = *cx.theme();
1955        let s = self.shared;
1956        let (index, row) = (self.index, self.row);
1957        let selected = s.selected.contains(&row);
1958        let hovered = cx.is_hover_within();
1959        let rh = s.grid.row_height;
1960        let mode = s.grid.selection_mode;
1961        let mut cells = Vec::with_capacity(s.visible.len() + 1);
1962        let mut fixed_cells = Vec::new();
1963        let control = theme.color(Semantic::Control);
1964        let fill = if selected {
1965            theme.color(Semantic::Accent).with_alpha(SELECTED_ROW)
1966        } else if hovered {
1967            control.faded(HOVERED_ROW)
1968        } else if self.even {
1969            s.grid.even_row.unwrap_or_else(|| {
1970                if s.grid.striped { control.faded(STRIPED_ROW) } else { Color::TRANSPARENT }
1971            })
1972        } else {
1973            s.grid.odd_row.unwrap_or(Color::TRANSPARENT)
1974        };
1975        let line = if s.grid.horizontal_lines {
1976            theme.color(Semantic::Border).faded(if s.grid.striped { 0.5 } else { 0.6 })
1977        } else {
1978            Color::TRANSPARENT
1979        };
1980        // What stays in place has to cover what scrolls under it.
1981        let surface = theme.color(Semantic::Surface);
1982        let solid = move |edge: Option<Color>| -> Paint {
1983            Paint::Custom(Rc::new(move |f: Rect, scene| {
1984                scene.push_quad(Quad::new(f, surface));
1985                scene.push_quad(Quad::new(f, fill));
1986                scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
1987                if let Some(edge) = edge {
1988                    scene.push_quad(Quad::new(Rect::new(f.max_x() - 1.0, f.y(), 1.0, f.height()), edge));
1989                }
1990            }))
1991        };
1992        let select_fixed = s.fixed_width > 0.0;
1993        if s.select_width > 0.0 {
1994            let mut leading = Vec::with_capacity(2);
1995            if s.grid.row_detail.is_some() {
1996                let open = s.h.expanded.get().contains(&row);
1997                let chevron = if open { IconName::ChevronDown } else { IconName::ChevronRight };
1998                let glyph = util::icon(cx, chevron, 16.0, theme.color(Semantic::InkMuted));
1999                let arrow = cx.container(
2000                    LayoutStyle {
2001                        width: Some(DETAIL_ARROW_WIDTH),
2002                        height: Some(rh),
2003                        align: Align::Center,
2004                        justify: Justify::Center,
2005                        ..LayoutStyle::stack(Axis::Horizontal)
2006                    },
2007                    &[glyph],
2008                    Paint::None,
2009                );
2010                let labels = cx.labels();
2011                let name = if open { &labels.grid_collapse_row } else { &labels.grid_expand_row };
2012                cx.semantics(arrow, Semantics::new(Role::Button).name(name.clone()).expanded(open));
2013                let (expanded, h) = (s.h.expanded.clone(), s.h.clone());
2014                cx.on_click(
2015                    arrow,
2016                    Rc::new(move || {
2017                        h.focus_row.set(Some(index));
2018                        expanded.update(|set| {
2019                            if !set.remove(&row) {
2020                                set.insert(row);
2021                            }
2022                        });
2023                    }),
2024                );
2025                cx.cursor(arrow, CursorIcon::Pointer);
2026                leading.push(arrow);
2027            }
2028            if mode == SelectionMode::Multiple {
2029                let b = crate::inputs::check_box(cx, &theme, selected, false);
2030                let check = cx.container(
2031                    LayoutStyle {
2032                        width: Some(CHECK_WIDTH),
2033                        height: Some(rh),
2034                        align: Align::Center,
2035                        justify: Justify::Center,
2036                        ..LayoutStyle::stack(Axis::Horizontal)
2037                    },
2038                    &[b],
2039                    Paint::None,
2040                );
2041                let name = cx.labels().grid_select_row.clone();
2042                cx.semantics(check, Semantics::new(Role::CheckBox).name(name).toggled(selected));
2043                let (sel, h) = (s.h.selection.clone(), s.h.clone());
2044                cx.on_click(
2045                    check,
2046                    Rc::new(move || {
2047                        sel.update(|set| {
2048                            if !set.remove(&row) {
2049                                set.insert(row);
2050                            }
2051                        });
2052                        h.focus_row.set(Some(index));
2053                        h.anchor.set(Some(index));
2054                    }),
2055                );
2056                cx.cursor(check, CursorIcon::Pointer);
2057                leading.push(check);
2058            }
2059            let cell = cx.container(
2060                LayoutStyle {
2061                    width: Some(s.select_width),
2062                    height: Some(rh),
2063                    absolute: select_fixed.then_some((0.0, 0.0)),
2064                    ..LayoutStyle::stack(Axis::Horizontal)
2065                },
2066                &leading,
2067                if select_fixed { solid(None) } else { Paint::None },
2068            );
2069            if select_fixed {
2070                cx.translate(cell, Point::new(s.offset_x, 0.0));
2071                fixed_cells.push(cell);
2072            } else {
2073                cells.push(cell);
2074            }
2075        }
2076        let attrs = cell_attrs(&theme);
2077        let first_column = s.visible.first().copied();
2078        for &c in s.visible {
2079            let col = &s.grid.columns[c];
2080            let value = self.values.get(c).cloned().unwrap_or_default();
2081            let looks = s.grid.cell_style.as_ref().map(|style| style(&self.values, c)).unwrap_or_default();
2082            let content = if s.editing == Some((row, c)) {
2083                let (data, buffer, editing, column) =
2084                    (s.grid.data.clone(), s.h.edit_buffer.clone(), s.h.editing.clone(), col.clone());
2085                let previous = value.clone();
2086                let (cancel, wrong, right) =
2087                    (s.h.editing.clone(), s.h.edit_error.clone(), s.h.edit_error.clone());
2088                let (check, local) = (s.grid.on_cell_edit.clone(), s.grid.remote.is_none());
2089                cx.child(
2090                    &TextField::new(s.h.edit_buffer.binding())
2091                        .compact()
2092                        .autofocus()
2093                        .on_submit(move |text| {
2094                            let value = column.parse(&text, &previous);
2095                            if let Some(check) = &check {
2096                                let edit = CellEdit {
2097                                    row,
2098                                    column: c,
2099                                    previous: previous.clone(),
2100                                    value: value.clone(),
2101                                };
2102                                if let Err(message) = check(&edit) {
2103                                    wrong.set(Some(message));
2104                                    return;
2105                                }
2106                            }
2107                            right.set(None);
2108                            if local {
2109                                data.set_cell(row, c, value);
2110                            }
2111                            buffer.set(String::new());
2112                            editing.set(None);
2113                        })
2114                        .on_cancel({
2115                            let wrong = s.h.edit_error.clone();
2116                            move || {
2117                                wrong.set(None);
2118                                cancel.set(None);
2119                            }
2120                        }),
2121                )
2122            } else if let Some(template) = &col.template {
2123                let view = template(&value);
2124                let inner = cx.keyed_child(c as u64, view.as_ref());
2125                // A neutral box, so the template's own cross-axis alignment cannot pull it off center.
2126                cx.container(LayoutStyle::stack(Axis::Horizontal), &[inner], Paint::None)
2127            } else {
2128                let ink = if value.is_empty() {
2129                    theme.color(Semantic::InkMuted)
2130                } else {
2131                    theme.color(looks.ink.unwrap_or(Semantic::Ink))
2132                };
2133                let text = if value.is_empty() { "โ€”".to_owned() } else { col.display(&value) };
2134                let mut attrs = attrs.clone();
2135                if looks.bold {
2136                    attrs.weight = Weight::SEMIBOLD.0;
2137                }
2138                cx.text(&text, &attrs, ink, LayoutStyle::leaf())
2139            };
2140            let mut style = cell_style(s.widths[c], rh, col.align);
2141            if s.editing == Some((row, c)) {
2142                style.align = Align::Center;
2143                style.padding = Insets::symmetric(0.0, 4.0);
2144                style.justify = Justify::Start;
2145            } else if first_column == Some(c) {
2146                style.padding.left += s.indent;
2147            }
2148            let fixed = s.fixed_x(c);
2149            style.absolute = fixed.map(|x| (x, 0.0));
2150            let paint = if fixed.is_some() {
2151                solid((s.last_fixed == Some(c)).then(|| theme.color(Semantic::Border)))
2152            } else if let Some(fill) = looks.fill {
2153                Paint::Quad(QuadPaint::new(theme.color(fill).with_alpha(0.14)))
2154            } else {
2155                Paint::None
2156            };
2157            let cell = cx.container(LayoutStyle { align: Align::Center, ..style }, &[content], paint);
2158            if s.editing != Some((row, c)) {
2159                cx.semantics(cell, Semantics::new(Role::Cell));
2160            }
2161            let result = s.result.clone();
2162            let h = s.h.clone();
2163            cx.on_click(cell, Rc::new(move || click_row(&result, &h, mode, row, index)));
2164            let (editable, h2, activate) = (
2165                col.editable && (s.grid.remote.is_none() || s.grid.on_cell_edit.is_some()),
2166                s.h.clone(),
2167                s.grid.on_row_activate.clone(),
2168            );
2169            let rows_value = col.display(&value);
2170            cx.on_double_click(
2171                cell,
2172                Rc::new(move || {
2173                    if editable {
2174                        h2.edit_buffer.set(rows_value.clone());
2175                        h2.editing.set(Some((row, c)));
2176                    } else if let Some(a) = &activate {
2177                        a(row);
2178                    }
2179                }),
2180            );
2181            if fixed.is_some() {
2182                cx.translate(cell, Point::new(s.offset_x, 0.0));
2183                fixed_cells.push(cell);
2184            } else {
2185                cells.push(cell);
2186            }
2187        }
2188        if !s.grid.row_actions.is_empty() {
2189            let name_column = s.grid.name_column.or(s.grid.key_column).or(first_column).unwrap_or(0);
2190            let row_name = match (s.grid.columns.get(name_column), self.values.get(name_column)) {
2191                (Some(column), Some(value)) => column.display(value),
2192                _ => String::new(),
2193            };
2194            let mut buttons = Vec::with_capacity(s.grid.row_actions.len());
2195            for (i, action) in s.grid.row_actions.iter().enumerate() {
2196                let (run, values) = (action.on_click.clone(), self.values.clone());
2197                let enabled = action.enabled.as_ref().is_none_or(|f| f(&self.values));
2198                buttons.push(
2199                    cx.keyed_child(
2200                        (1 << 40) + i as u64,
2201                        &crate::IconButton::new(action.icon)
2202                            .label(format!("{} {row_name}", action.label))
2203                            .size(ButtonSize::Small)
2204                            .disabled(!enabled)
2205                            .on_click(move || run(&values)),
2206                    ),
2207                );
2208            }
2209            cells.push(cx.container(
2210                LayoutStyle {
2211                    width: Some(s.grid.row_actions.len() as f32 * ROW_ACTION_WIDTH),
2212                    height: Some(rh),
2213                    align: Align::Center,
2214                    ..LayoutStyle::stack(Axis::Horizontal)
2215                },
2216                &buttons,
2217                Paint::None,
2218            ));
2219        }
2220        if s.fixed_width > 0.0 {
2221            let room = cx.container(
2222                LayoutStyle { width: Some(s.fixed_width), height: Some(rh), ..LayoutStyle::leaf() },
2223                &[],
2224                Paint::None,
2225            );
2226            cells.insert(0, room);
2227        }
2228        cells.extend(fixed_cells);
2229        let focus = (s.focus_row == Some(index)).then(|| theme.color(Semantic::Accent));
2230        let column_lines = s.lines.clone();
2231        let node = cx.container(
2232            LayoutStyle {
2233                width: Some(s.total_width),
2234                height: Some(rh),
2235                ..LayoutStyle::stack(Axis::Horizontal)
2236            },
2237            &cells,
2238            Paint::Custom(Rc::new(move |f: Rect, scene| {
2239                scene.push_quad(Quad::new(f, fill));
2240                scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2241                for x in column_lines.iter() {
2242                    scene.push_quad(Quad::new(Rect::new(f.x() + x, f.y(), 1.0, f.height()), line));
2243                }
2244                if let Some(accent) = focus {
2245                    scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), 2.0, f.height()), accent));
2246                }
2247            })),
2248        );
2249        // The grid body keeps focus; the keyboard row is announced as active.
2250        let mut semantics = Semantics::new(Role::Row).active(s.focus_row == Some(index));
2251        if mode != SelectionMode::None {
2252            semantics = semantics.selected(selected);
2253        }
2254        cx.semantics(node, semantics);
2255        node
2256    }
2257}
2258
2259/// What a row shows below itself when open. It stays in sight while the columns scroll.
2260struct DetailRow<'a> {
2261    row: usize,
2262    values: &'a Row,
2263    shared: &'a Shared<'a>,
2264    width: f32,
2265}
2266
2267impl View for DetailRow<'_> {
2268    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2269        let theme = *cx.theme();
2270        let s = self.shared;
2271        let Some(detail) = &s.grid.row_detail else { return cx.empty() };
2272        let content = detail(self.values, self.row);
2273        let inner = cx.child(content.as_ref());
2274        let holder = cx.container(
2275            LayoutStyle {
2276                width: Some(self.width),
2277                height: Some(s.grid.detail_height),
2278                padding: Insets::uniform(12.0),
2279                overflow_hidden: true,
2280                align: Align::Stretch,
2281                ..LayoutStyle::stack(Axis::Vertical)
2282            },
2283            &[inner],
2284            Paint::None,
2285        );
2286        cx.clip(holder);
2287        cx.translate(holder, Point::new(s.offset_x, 0.0));
2288        let (base, line) = (theme.color(Semantic::Canvas), theme.color(Semantic::Border).faded(0.6));
2289        cx.container(
2290            LayoutStyle {
2291                width: Some(s.total_width),
2292                height: Some(s.grid.detail_height),
2293                ..LayoutStyle::stack(Axis::Horizontal)
2294            },
2295            &[holder],
2296            Paint::Custom(Rc::new(move |f: Rect, scene| {
2297                scene.push_quad(Quad::new(f, base));
2298                scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2299            })),
2300        )
2301    }
2302}
2303
2304struct GroupRow<'a> {
2305    index: usize,
2306    shared: &'a Shared<'a>,
2307}
2308
2309impl View for GroupRow<'_> {
2310    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2311        let theme = *cx.theme();
2312        let s = self.shared;
2313        let ViewRow::Group { level, column, label, key, count, collapsed, summaries } =
2314            &s.result.rows[self.index]
2315        else {
2316            return cx.empty();
2317        };
2318        let col = &s.grid.columns[*column];
2319        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
2320        let chevron = if *collapsed { IconName::ChevronRight } else { IconName::ChevronDown };
2321        let mut children = vec![util::icon(cx, chevron, 16.0, muted)];
2322        let attrs = cell_attrs(&theme);
2323        children.push(cx.text(&format!("{}:", col.title), &attrs, muted, LayoutStyle::leaf()));
2324        let labels = cx.labels();
2325        let shown = if label.is_empty() { labels.grid_empty.as_str() } else { label.as_str() };
2326        let mut bold = attrs.clone();
2327        bold.weight = Weight::SEMIBOLD.0;
2328        children.push(cx.text(shown, &bold, ink, LayoutStyle::leaf()));
2329        let badge = cx.child(&Badge::new(crate::grid::model::NumberFormat::default().format(*count as f64)));
2330        children.push(cx.container(LayoutStyle::stack(Axis::Horizontal), &[badge], Paint::None));
2331        let rh = s.grid.row_height;
2332        let left = 12.0 + *level as f32 * GROUP_INDENT;
2333        let title = cx.container(
2334            LayoutStyle {
2335                height: Some(rh),
2336                padding: Insets { left, ..Insets::ZERO },
2337                spacing: 8.0,
2338                align: Align::Center,
2339                ..LayoutStyle::stack(Axis::Horizontal)
2340            },
2341            &children,
2342            Paint::None,
2343        );
2344        // The title stays in sight while the columns scroll.
2345        cx.translate(title, Point::new(s.offset_x, 0.0));
2346        // Each summary under its own column, where one can see them line up.
2347        let title_ends =
2348            left + 24.0 + (col.title.chars().count() + shown.chars().count() + 8) as f32 * CHAR_WIDTH;
2349        let mut parts = vec![title];
2350        let mut spoken = Vec::new();
2351        let range = s.ranges.get(self.index).copied().unwrap_or_default();
2352        for &c in s.visible {
2353            let other = &s.grid.columns[c];
2354            let figure = match s.formula_text(c, range) {
2355                Some(text) => Some((text, labels.grid_summary_formula.clone())),
2356                None => {
2357                    summaries.get(c).copied().flatten().zip(s.arrangement.summaries[c]).map(|(v, summary)| {
2358                        (other.display_summary(summary, v), labels.summary(summary).to_owned())
2359                    })
2360                }
2361            };
2362            if let Some((text, kind)) = figure {
2363                spoken.push(format!("{kind} {}: {text}", other.title));
2364                let x = s.x_of(c);
2365                if x < title_ends {
2366                    continue;
2367                }
2368                let value = cx.text(&text, &bold, ink, LayoutStyle::leaf());
2369                let mut style = cell_style(s.widths[c], rh, other.align);
2370                style.absolute = Some((x, 0.0));
2371                parts.push(cx.container(style, &[value], Paint::None));
2372            }
2373        }
2374        let hovered = cx.is_hover_within();
2375        let base = theme.color(Semantic::Canvas);
2376        let fill = if hovered { base.lerp(ink, 0.04) } else { base };
2377        let line = theme.color(Semantic::Border).faded(0.6);
2378        let node = cx.container(
2379            LayoutStyle {
2380                width: Some(s.total_width),
2381                height: Some(rh),
2382                align: Align::Center,
2383                ..LayoutStyle::stack(Axis::Horizontal)
2384            },
2385            &parts,
2386            Paint::Custom(Rc::new(move |f: Rect, scene| {
2387                scene.push_quad(Quad::new(f, fill));
2388                scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2389            })),
2390        );
2391        let (q, key, focus, index) = (s.h.query.clone(), key.clone(), s.h.focus_row.clone(), self.index);
2392        cx.on_click(
2393            node,
2394            Rc::new(move || {
2395                focus.set(Some(index));
2396                q.update(|q| q.toggle_collapsed(&key));
2397            }),
2398        );
2399        cx.semantics(
2400            node,
2401            Semantics::new(Role::Row)
2402                .name(format!("{}: {shown}, {count} {}", col.title, labels.grid_rows))
2403                .description(spoken.join(", "))
2404                .expanded(!*collapsed)
2405                .active(s.focus_row == Some(self.index)),
2406        );
2407        cx.cursor(node, CursorIcon::Pointer);
2408        node
2409    }
2410}
2411
2412#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
2413impl DataGrid {
2414    fn handles(&self, cx: &mut BuildCx<'_>) -> Handles {
2415        let initial = {
2416            let mut q = self.initial.clone();
2417            q.hidden = self.columns.iter().map(|c| !c.visible).collect();
2418            q
2419        };
2420        let widths0: Vec<f32> = self.columns.iter().map(|c| c.width).collect();
2421        let query = cx.use_state(move || initial);
2422        let widths = cx.use_state(move || widths0);
2423        let first_arrangement =
2424            Arrangement::new(&self.columns, self.group_panel.unwrap_or(false), self.page_size);
2425        let arrange = cx.use_state(move || first_arrangement);
2426        let own_selection: State<BTreeSet<usize>> = cx.use_state(BTreeSet::new);
2427        let focus_row = cx.use_state(|| None);
2428        let anchor = cx.use_state(|| Rc::new(Cell::new(None))).get();
2429        let reveal = cx.use_state(|| Rc::new(Cell::new(None))).get();
2430        let editing = cx.use_state(|| None);
2431        let edit_buffer = cx.use_state(String::new);
2432        let edit_error = cx.use_state(|| None);
2433        let resize = cx.use_state(|| Rc::new(Cell::new(None))).get();
2434        Handles {
2435            query,
2436            widths,
2437            selection: match (&self.selected_keys, self.key_column) {
2438                (Some(keys), Some(column)) => {
2439                    keyed_selection(keys.clone(), column, self.data.clone(), self.remote.clone())
2440                }
2441                _ => self.selection.clone().unwrap_or_else(|| own_selection.binding()),
2442            },
2443            focus_row,
2444            anchor,
2445            reveal,
2446            editing,
2447            edit_buffer,
2448            edit_error,
2449            resize,
2450            modifiers: cx.modifiers(),
2451            building_filter: cx.use_state(|| false),
2452            expanded: {
2453                let local = cx.use_state(BTreeSet::new);
2454                self.expanded_rows.clone().unwrap_or_else(|| local.binding())
2455            },
2456            arrange,
2457            drag: cx.use_state(|| None),
2458            press: cx.use_state(|| Rc::new(Cell::new((Point::ZERO, false)))).get(),
2459            dragged: cx.use_state(|| Rc::new(Cell::new(false))).get(),
2460            zones: cx.use_state(|| Rc::new(RefCell::new(Zones::default()))).get(),
2461            menu: cx.use_state(|| None),
2462            filter_open: cx.use_state(|| None),
2463            filter_chosen: cx.use_state(BTreeSet::new),
2464            filter_search: cx.use_state(String::new),
2465            page: cx.use_state(|| 0),
2466            saving: cx.use_state(|| false),
2467            scenario_name: cx.use_state(String::new),
2468            active_scenario: cx.use_state(|| None),
2469            original: cx.use_state(|| Rc::new(RefCell::new(None))).get(),
2470            seen_layout: cx.use_state(|| Rc::new(RefCell::new(None))).get(),
2471            summary_edit: cx.use_state(|| None),
2472            summary_draft: cx.use_state(String::new),
2473            formula_cache: cx.use_state(|| Rc::new(RefCell::new(FormulaCache::default()))).get(),
2474        }
2475    }
2476
2477    fn toolbar_view(
2478        &self,
2479        cx: &mut BuildCx<'_>,
2480        h: &Handles,
2481        result: &QueryResult,
2482        rows: &Rc<Vec<Row>>,
2483        total: usize,
2484    ) -> NodeId {
2485        let theme = *cx.theme();
2486        let labels = cx.labels();
2487        let mut items = Vec::new();
2488        if self.search_panel {
2489            let (read, write) = (h.query.clone(), h.query.clone());
2490            let search = Binding::new(
2491                move || read.with(|q| q.search.clone()),
2492                move |t| write.update(|q| q.search = t),
2493            );
2494            items.push(
2495                cx.child(
2496                    &TextField::new(search)
2497                        .placeholder(labels.grid_search.clone())
2498                        .icon(IconName::Search)
2499                        .compact()
2500                        .width(240.0),
2501                ),
2502            );
2503        }
2504        let query = h.query.peek().clone();
2505        if self.remote.is_none() && !h.arrange.peek().group_panel {
2506            let mut group =
2507                DropdownMenu::new(labels.grid_group.clone()).icon(IconName::Layers).size(ButtonSize::Small);
2508            for (c, col) in
2509                self.columns.iter().enumerate().filter(|(c, col)| col.groupable && !query.is_hidden(*c))
2510            {
2511                let q = h.query.clone();
2512                let mut item =
2513                    MenuItem::new(col.title.clone()).on_select(move || q.update(|q| q.toggle_group(c)));
2514                if query.group_by.contains(&c) {
2515                    item = item.icon(IconName::Check);
2516                }
2517                group = group.item(item);
2518            }
2519            if !query.group_by.is_empty() {
2520                let q = h.query.clone();
2521                group = group.separator().item(
2522                    MenuItem::new(labels.grid_clear_grouping.clone()).icon(IconName::X).on_select(
2523                        move || {
2524                            q.update(|q| {
2525                                q.group_by.clear();
2526                                q.collapsed.clear();
2527                            })
2528                        },
2529                    ),
2530                );
2531            }
2532            items.push(cx.child(&group));
2533        }
2534        if let Some(scenarios) = &self.scenarios {
2535            items.push(self.scenario_menu(cx, h, scenarios));
2536        }
2537        let mut chooser =
2538            DropdownMenu::new(labels.grid_columns.clone()).icon(IconName::Table).size(ButtonSize::Small);
2539        for (c, col) in self.columns.iter().enumerate() {
2540            let q = h.query.clone();
2541            let item = MenuItem::new(col.title.clone()).on_select(move || {
2542                q.update(|q| {
2543                    if q.hidden.len() <= c {
2544                        q.hidden.resize(c + 1, false);
2545                    }
2546                    q.hidden[c] = !q.hidden[c];
2547                })
2548            });
2549            chooser = chooser.item(if query.is_hidden(c) { item } else { item.icon(IconName::Check) });
2550        }
2551        items.push(cx.child(&chooser));
2552        if !query.sort.is_empty()
2553            || query.filters.iter().any(|f| !f.trim().is_empty())
2554            || !query.search.is_empty()
2555            || !query.group_by.is_empty()
2556        {
2557            let q = h.query.clone();
2558            items.push(
2559                cx.child(
2560                    &Button::new(labels.grid_reset.clone())
2561                        .style(ButtonStyle::Ghost)
2562                        .size(ButtonSize::Small)
2563                        .icon(IconName::RotateCcw)
2564                        .on_click(move || {
2565                            q.update(|q| {
2566                                q.sort.clear();
2567                                q.filters.clear();
2568                                q.search.clear();
2569                                q.group_by.clear();
2570                                q.collapsed.clear();
2571                            })
2572                        }),
2573                ),
2574            );
2575        }
2576        if let Some(add) = self.on_add_row.clone() {
2577            items.push(
2578                cx.child(
2579                    &Button::new(labels.grid_add_row.clone())
2580                        .style(ButtonStyle::Secondary)
2581                        .size(ButtonSize::Small)
2582                        .icon(IconName::Plus)
2583                        .on_click(move || add()),
2584                ),
2585            );
2586        }
2587        if let (Some(delete), false) = (self.on_delete_rows.clone(), h.selection.get().is_empty()) {
2588            let selection = h.selection.clone();
2589            items.push(
2590                cx.child(
2591                    &Button::new(labels.grid_delete_rows.clone())
2592                        .style(ButtonStyle::Ghost)
2593                        .size(ButtonSize::Small)
2594                        .icon(IconName::Trash2)
2595                        .on_click(move || delete(selection.get().into_iter().collect())),
2596                ),
2597            );
2598        }
2599        items.push(cx.child(&Spacer::new()));
2600        if let Some(message) = h.edit_error.get() {
2601            items.push(util::label(cx, &message, TextStyle::Callout, None, theme.color(Semantic::Danger)));
2602        }
2603        let selected = h.selection.get().len();
2604        let fmt = crate::grid::model::NumberFormat::default();
2605        let mut status = format!(
2606            "{} {} {} {}",
2607            fmt.format(result.matched.len() as f64),
2608            labels.grid_of,
2609            fmt.format(rows.len() as f64),
2610            labels.grid_rows
2611        );
2612        if self.remote.is_some() {
2613            status = format!("{} {}", fmt.format(total as f64), labels.grid_rows);
2614        }
2615        // In place of the count, not after it: a longer line would push the bar onto a second row
2616        // and move the row just clicked out from under the pointer.
2617        if selected > 0 {
2618            status = format!("{} {}", fmt.format(selected as f64), labels.grid_selected);
2619        }
2620        items.push(util::label(cx, &status, TextStyle::Callout, None, theme.color(Semantic::InkMuted)));
2621        if let Some(export) = &self.on_export {
2622            let (export, columns, rows, q, matched) =
2623                (export.clone(), self.columns.clone(), rows.clone(), query.clone(), result.matched.clone());
2624            items.push(
2625                cx.child(
2626                    &Button::new(labels.grid_export.clone())
2627                        .style(ButtonStyle::Secondary)
2628                        .size(ButtonSize::Small)
2629                        .icon(IconName::Download)
2630                        .on_click(move || export(to_csv(&columns, &rows, &q, &matched))),
2631                ),
2632            );
2633        }
2634        if self.filter_builder {
2635            let open = h.building_filter.clone();
2636            items.push(
2637                cx.child(
2638                    &Button::new(labels.grid_filter_label.clone())
2639                        .style(ButtonStyle::Secondary)
2640                        .size(ButtonSize::Small)
2641                        .icon(IconName::Filter)
2642                        .on_click(move || open.set(true)),
2643                ),
2644            );
2645        }
2646        if let Some(export) = &self.on_export_file {
2647            let title = self.export_title.clone().unwrap_or_else(|| labels.grid_name.clone());
2648            let culture = labels.culture.clone();
2649            let table = Rc::new(move |columns: &[GridColumn], rows: &[Row], q: &GridQuery, m: &[usize]| {
2650                to_table(&title, &culture, columns, rows, q, m)
2651            });
2652            let mut menu = DropdownMenu::new(labels.grid_export_file.clone())
2653                .icon(IconName::Download)
2654                .size(ButtonSize::Small)
2655                .style(ButtonStyle::Secondary);
2656            for (format, text) in [
2657                (GridExportFormat::Xlsx, "Excel (.xlsx)"),
2658                (GridExportFormat::Pdf, "PDF (.pdf)"),
2659                (GridExportFormat::Csv, "CSV (.csv)"),
2660            ] {
2661                let (export, table, columns, rows, q, matched) = (
2662                    export.clone(),
2663                    table.clone(),
2664                    self.columns.clone(),
2665                    rows.clone(),
2666                    query.clone(),
2667                    result.matched.clone(),
2668                );
2669                menu = menu.item(
2670                    MenuItem::new(text)
2671                        .on_select(move || export(format, table(&columns, &rows, &q, &matched))),
2672                );
2673            }
2674            items.push(cx.child(&menu));
2675        }
2676        cx.container(
2677            LayoutStyle {
2678                padding: Insets::symmetric(10.0, 12.0),
2679                spacing: 8.0,
2680                align: Align::Center,
2681                wrap: true,
2682                ..LayoutStyle::stack(Axis::Horizontal)
2683            },
2684            &items,
2685            Paint::None,
2686        )
2687    }
2688
2689    fn scenario_menu(
2690        &self,
2691        cx: &mut BuildCx<'_>,
2692        h: &Handles,
2693        scenarios: &Binding<Vec<GridScenario>>,
2694    ) -> NodeId {
2695        let labels = cx.labels();
2696        let active = h.active_scenario.get();
2697        let title = match &active {
2698            Some(name) => format!("{}: {name}", labels.grid_scenario),
2699            None => labels.grid_scenario.clone(),
2700        };
2701        let mut menu = DropdownMenu::new(title).icon(IconName::Bookmark).size(ButtonSize::Small);
2702        let columns = self.columns.clone();
2703        for scenario in scenarios.get() {
2704            let on = active.as_deref() == Some(scenario.name.as_str());
2705            let (h2, columns, name) = (h.clone(), columns.clone(), scenario.name.clone());
2706            let item = MenuItem::new(scenario.name.clone()).on_select(move || {
2707                apply_layout(&h2, &columns, &scenario.layout);
2708                h2.active_scenario.set(Some(name.clone()));
2709            });
2710            menu = menu.item(if on { item.icon(IconName::Check) } else { item });
2711        }
2712        if !scenarios.get().is_empty() {
2713            menu = menu.separator();
2714        }
2715        let saving = h.saving.clone();
2716        let name = h.scenario_name.clone();
2717        menu = menu.item(MenuItem::new(labels.grid_scenario_save.clone()).icon(IconName::Save).on_select(
2718            move || {
2719                name.set(String::new());
2720                saving.set(true);
2721            },
2722        ));
2723        if let Some(current) = active {
2724            let (h2, columns, list, shown) = (h.clone(), columns.clone(), scenarios.clone(), current.clone());
2725            menu = menu.item(
2726                MenuItem::new(format!("{} {shown}", labels.grid_scenario_update))
2727                    .icon(IconName::Check)
2728                    .on_select(move || {
2729                        let layout = current_layout(&h2, &columns);
2730                        list.update(|all| {
2731                            if let Some(s) = all.iter_mut().find(|s| s.name == current) {
2732                                s.layout = layout.clone();
2733                            }
2734                        });
2735                    }),
2736            );
2737            let (active, list, shown2) = (h.active_scenario.clone(), scenarios.clone(), shown.clone());
2738            menu = menu.item(
2739                MenuItem::new(format!("{} {shown}", labels.grid_scenario_delete))
2740                    .icon(IconName::Trash2)
2741                    .destructive()
2742                    .on_select(move || {
2743                        list.update(|all| all.retain(|s| s.name != shown2));
2744                        active.set(None);
2745                    }),
2746            );
2747        }
2748        let (h2, columns) = (h.clone(), columns);
2749        menu = menu.separator().item(
2750            MenuItem::new(labels.grid_scenario_original.clone()).icon(IconName::RotateCcw).on_select(
2751                move || {
2752                    if let Some(original) = h2.original.borrow().clone() {
2753                        apply_layout(&h2, &columns, &original);
2754                    }
2755                    h2.active_scenario.set(None);
2756                },
2757            ),
2758        );
2759        cx.child(&menu)
2760    }
2761
2762    fn filter_dialog(&self, cx: &mut BuildCx<'_>, h: &Handles, rows: &[Row]) -> NodeId {
2763        let columns: Rc<Vec<GridColumn>> = Rc::new(self.columns.clone());
2764        // The values to pick from cost a pass over the rows, so only an open editor pays it.
2765        let open = h.building_filter.get();
2766        let fields = self.columns.iter().enumerate().filter(|(_, c)| c.filterable).map(|(i, column)| {
2767            let kind = if column.number.is_some() {
2768                crate::FilterFieldKind::Number
2769            } else {
2770                match rows.iter().filter_map(|r| r.get(i)).find(|v| !v.is_empty()) {
2771                    Some(Value::Bool(_)) => crate::FilterFieldKind::Boolean,
2772                    Some(Value::Date(_)) => crate::FilterFieldKind::Date,
2773                    Some(Value::Number(_)) => crate::FilterFieldKind::Number,
2774                    _ => crate::FilterFieldKind::Text,
2775                }
2776            };
2777            let values = if open && kind != crate::FilterFieldKind::Boolean {
2778                values_of(rows, column, i)
2779                    .into_iter()
2780                    .filter_map(|(shown, value)| {
2781                        let key = match value {
2782                            Value::Text(text) => text,
2783                            Value::Number(n) => n.to_string(),
2784                            Value::Date(date) => date.to_string(),
2785                            _ => return None,
2786                        };
2787                        Some(crate::LookupItem::new(key, shown))
2788                    })
2789                    .collect()
2790            } else {
2791                Vec::new()
2792            };
2793            crate::FilterField::new(column.title.clone(), column.title.clone(), kind).values(values)
2794        });
2795        let (read, write) = (h.query.clone(), h.query.clone());
2796        let text = Binding::new(
2797            move || read.with(whole_filter),
2798            move |text: String| write.update(|q| set_whole_filter(q, &columns, text)),
2799        );
2800        cx.child(
2801            &Dialog::new(h.building_filter.binding(), cx.labels().grid_edit_filter.clone())
2802                .content(crate::FilterBuilder::new(text, fields))
2803                .width(640.0),
2804        )
2805    }
2806
2807    fn scenario_dialog(
2808        &self,
2809        cx: &mut BuildCx<'_>,
2810        h: &Handles,
2811        scenarios: &Binding<Vec<GridScenario>>,
2812    ) -> NodeId {
2813        let labels = cx.labels();
2814        let (h2, columns, list) = (h.clone(), self.columns.clone(), scenarios.clone());
2815        let save = move || {
2816            let name = h2.scenario_name.get().trim().to_owned();
2817            if name.is_empty() {
2818                return;
2819            }
2820            let layout = current_layout(&h2, &columns);
2821            list.update(|all| {
2822                all.retain(|s| s.name != name);
2823                all.push(GridScenario::new(name.clone(), layout.clone()));
2824            });
2825            h2.active_scenario.set(Some(name));
2826            h2.saving.set(false);
2827        };
2828        let on_enter = save.clone();
2829        cx.child(
2830            &Dialog::new(h.saving.binding(), labels.grid_scenario_save.trim_end_matches('โ€ฆ').to_owned())
2831                .content(
2832                    TextField::new(h.scenario_name.binding())
2833                        .placeholder(labels.grid_scenario_name.clone())
2834                        .autofocus()
2835                        .on_submit(move |_| on_enter()),
2836                )
2837                .action(Button::new(labels.grid_save.clone()).on_click(save))
2838                .width(380.0),
2839        )
2840    }
2841
2842    fn group_panel_view(
2843        &self,
2844        cx: &mut BuildCx<'_>,
2845        h: &Handles,
2846        rows: &Rc<Vec<Row>>,
2847        facts: &Rc<Vec<(String, bool)>>,
2848    ) -> Option<NodeId> {
2849        let query = h.query.peek().clone();
2850        // What is grouped always shows; the place to drag to only when asked for.
2851        if self.remote.is_some() || (!h.arrange.peek().group_panel && query.group_by.is_empty()) {
2852            return None;
2853        }
2854        let theme = *cx.theme();
2855        let labels = cx.labels();
2856        let muted = theme.color(Semantic::InkMuted);
2857        let drag = *h.drag.peek();
2858        let target_place =
2859            drag.and_then(|d| if let DropTarget::Group(i) = d.target { Some(i) } else { None });
2860        let accent = theme.color(Semantic::Accent);
2861        let caret = |cx: &mut BuildCx<'_>| {
2862            cx.container(
2863                LayoutStyle { width: Some(2.0), height: Some(26.0), ..LayoutStyle::leaf() },
2864                &[],
2865                Paint::Quad(QuadPaint { corner_radius: 1.0, ..QuadPaint::new(accent) }),
2866            )
2867        };
2868        let moving = drag.and_then(|d| if let DragSource::Chip(c) = d.source { Some(c) } else { None });
2869        let mut items = Vec::new();
2870        let mut place = 0;
2871        for (i, &c) in query.group_by.iter().enumerate() {
2872            if i > 0 {
2873                items.push(util::icon(cx, IconName::ChevronRight, 14.0, muted));
2874            }
2875            if target_place == Some(place) && moving != Some(c) {
2876                items.push(caret(cx));
2877            }
2878            items
2879                .push(cx.keyed_child(c as u64 + 1, &GroupChip { index: i, column: c, grid: self, h, facts }));
2880            if moving != Some(c) {
2881                place += 1;
2882            }
2883        }
2884        if target_place == Some(place) {
2885            items.push(caret(cx));
2886        }
2887        if query.group_by.is_empty() || drag.is_some() {
2888            items.push(util::label(cx, &labels.grid_drag_hint, TextStyle::Callout, None, muted));
2889        }
2890        items.push(cx.child(&Spacer::new()));
2891        if !query.group_by.is_empty() {
2892            let (q, columns, rows2) = (h.query.clone(), self.columns.clone(), rows.clone());
2893            items.push(
2894                cx.child(
2895                    &Button::new(labels.grid_collapse_all.clone())
2896                        .style(ButtonStyle::Ghost)
2897                        .size(ButtonSize::Small)
2898                        .on_click(move || {
2899                            let keys = super::query::group_keys(&columns, &rows2, &q.peek());
2900                            q.update(|q| q.collapsed = keys.clone());
2901                        }),
2902                ),
2903            );
2904            let q = h.query.clone();
2905            items.push(
2906                cx.child(
2907                    &Button::new(labels.grid_expand_all.clone())
2908                        .style(ButtonStyle::Ghost)
2909                        .size(ButtonSize::Small)
2910                        .on_click(move || q.update(|q| q.collapsed.clear())),
2911                ),
2912            );
2913        }
2914        let zones = h.zones.clone();
2915        let fill =
2916            if target_place.is_some() { accent.with_alpha(0.08) } else { theme.color(Semantic::Canvas) };
2917        let line = theme.color(Semantic::Border);
2918        Some(cx.container(
2919            LayoutStyle {
2920                min_height: Some(GROUP_PANEL_HEIGHT),
2921                padding: Insets::symmetric(6.0, 12.0),
2922                spacing: 6.0,
2923                align: Align::Center,
2924                wrap: true,
2925                align_self: Some(Align::Stretch),
2926                ..LayoutStyle::stack(Axis::Horizontal)
2927            },
2928            &items,
2929            Paint::Custom(Rc::new(move |f: Rect, scene| {
2930                zones.borrow_mut().panel = f;
2931                scene.push_quad(Quad::new(f, fill));
2932                scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), f.width(), 1.0), line));
2933                scene.push_quad(Quad::new(Rect::new(f.x(), f.max_y() - 1.0, f.width(), 1.0), line));
2934            })),
2935        ))
2936    }
2937
2938    /// A horizontally scrolled band (header, filter row, footer) kept in step
2939    /// with the body.
2940    fn band(
2941        cx: &mut BuildCx<'_>,
2942        cells: Vec<NodeId>,
2943        s: &Shared<'_>,
2944        offset_x: f32,
2945        fill: Color,
2946        top_line: bool,
2947        height: f32,
2948    ) -> NodeId {
2949        let theme = *cx.theme();
2950        let inner = cx.container(
2951            LayoutStyle {
2952                width: Some(s.total_width),
2953                height: Some(height),
2954                ..LayoutStyle::stack(Axis::Horizontal)
2955            },
2956            &cells,
2957            Paint::None,
2958        );
2959        let line = theme.color(Semantic::Border);
2960        let outer = cx.container(
2961            LayoutStyle {
2962                height: Some(height),
2963                overflow_hidden: true,
2964                align_self: Some(Align::Stretch),
2965                ..LayoutStyle::stack(Axis::Horizontal)
2966            },
2967            &[inner],
2968            Paint::Custom(Rc::new(move |f: Rect, scene| {
2969                scene.push_quad(Quad::new(f, fill));
2970                let y = if top_line { f.y() } else { f.max_y() - 1.0 };
2971                scene.push_quad(Quad::new(Rect::new(f.x(), y, f.width(), 1.0), line));
2972            })),
2973        );
2974        cx.clip(outer);
2975        cx.translate(outer, Point::new(-offset_x, 0.0));
2976        outer
2977    }
2978}
2979
2980fn key_handler(s: &Shared<'_>, mode: SelectionMode, clipboard: crate::Clipboard) -> crate::KeyHandler {
2981    let (result, h) = (s.result.clone(), s.h.clone());
2982    let visible: Vec<usize> = s.visible.to_vec();
2983    let columns: Vec<(usize, bool)> = s.visible.iter().map(|&c| (c, s.grid.columns[c].editable)).collect();
2984    let rows = s.rows_rc.clone();
2985    let display: Vec<GridColumn> = s.grid.columns.clone();
2986    let activate_row = s.grid.on_row_activate.clone();
2987    let delete_rows = s.grid.on_delete_rows.clone();
2988    let page_size = s.arrangement.page_size;
2989    let has_details = s.grid.row_detail.is_some();
2990    Rc::new(move |key, mods| {
2991        let len = result.rows.len();
2992        if len == 0 {
2993            return false;
2994        }
2995        let current = h.focus_row.get().unwrap_or(0).min(len - 1);
2996        let data_rows =
2997            || result.rows.iter().filter_map(|v| if let ViewRow::Data(r) = v { Some(*r) } else { None });
2998        if mods.ctrl || mods.meta {
2999            match key {
3000                // Copy the selected rows in view order, else the keyboard row.
3001                Key::Character(c) if c.eq_ignore_ascii_case("c") => {
3002                    let selection = h.selection.get();
3003                    let mut picked: Vec<usize> = data_rows().filter(|r| selection.contains(r)).collect();
3004                    if picked.is_empty()
3005                        && let ViewRow::Data(r) = result.rows[current]
3006                    {
3007                        picked.push(r);
3008                    }
3009                    if !picked.is_empty() {
3010                        clipboard.write(&to_tsv(&display, &rows, &visible, &picked));
3011                    }
3012                    return true;
3013                }
3014                Key::Character(c) if c.eq_ignore_ascii_case("a") && mode == SelectionMode::Multiple => {
3015                    h.selection.set(data_rows().collect());
3016                    return true;
3017                }
3018                _ => return false,
3019            }
3020        }
3021        // Enter / F2: edit the first editable cell, or activate the row, or toggle a group.
3022        let activate = |i: usize| match &result.rows[i] {
3023            ViewRow::Data(r) => {
3024                if let Some(&(c, _)) = columns.iter().find(|(_, editable)| *editable) {
3025                    let value = rows[*r].get(c).cloned().unwrap_or_default();
3026                    h.edit_buffer.set(display[c].display(&value));
3027                    h.editing.set(Some((*r, c)));
3028                } else if let Some(a) = &activate_row {
3029                    a(*r);
3030                }
3031            }
3032            ViewRow::Group { key, .. } => h.query.update(|q| q.toggle_collapsed(key)),
3033        };
3034        let move_to = |i: usize| {
3035            h.focus_row.set(Some(i));
3036            h.reveal.set(Some(i));
3037            if let Some(size) = page_size {
3038                h.page.set(i / size);
3039            }
3040            if let ViewRow::Data(r) = result.rows[i] {
3041                if mode == SelectionMode::Single {
3042                    h.selection.set(BTreeSet::from([r]));
3043                } else if mode == SelectionMode::Multiple && mods.shift {
3044                    h.selection.update(|s| {
3045                        s.insert(r);
3046                    });
3047                }
3048            }
3049        };
3050        match key {
3051            Key::ArrowDown => move_to((current + 1).min(len - 1)),
3052            Key::ArrowUp => move_to(current.saturating_sub(1)),
3053            Key::Home => move_to(0),
3054            Key::End => move_to(len - 1),
3055            Key::PageDown => move_to((current + 15).min(len - 1)),
3056            Key::PageUp => move_to(current.saturating_sub(15)),
3057            Key::Space => match &result.rows[current] {
3058                ViewRow::Data(r) if mode == SelectionMode::Multiple => h.selection.update(|s| {
3059                    if !s.remove(r) {
3060                        s.insert(*r);
3061                    }
3062                }),
3063                ViewRow::Group { key, .. } => h.query.update(|q| q.toggle_collapsed(key)),
3064                _ => return false,
3065            },
3066            Key::ArrowLeft | Key::ArrowRight => match &result.rows[current] {
3067                ViewRow::Group { key: group_key, collapsed, .. } => {
3068                    let expand = matches!(key, Key::ArrowRight);
3069                    if *collapsed == expand {
3070                        h.query.update(|q| q.toggle_collapsed(group_key));
3071                    }
3072                }
3073                ViewRow::Data(row) if has_details => {
3074                    set_details(&h.expanded, *row, matches!(key, Key::ArrowRight));
3075                }
3076                _ => return false,
3077            },
3078            Key::Enter => activate(current),
3079            Key::Delete if delete_rows.is_some() && !h.selection.get().is_empty() => {
3080                if let Some(delete) = &delete_rows {
3081                    delete(h.selection.get().into_iter().collect());
3082                }
3083            }
3084            Key::Other(k) if k.contains("F2") => activate(current),
3085            _ => return false,
3086        }
3087        true
3088    })
3089}
3090
3091fn current_layout(h: &Handles, columns: &[GridColumn]) -> GridLayout {
3092    layout::capture(columns, &h.query.peek(), &h.widths.peek(), &h.arrange.peek())
3093}
3094
3095fn apply_layout(h: &Handles, columns: &[GridColumn], wanted: &GridLayout) {
3096    let (mut query, mut widths, mut arrange) =
3097        (h.query.peek().clone(), h.widths.peek().clone(), h.arrange.peek().clone());
3098    layout::apply(wanted, columns, &mut query, &mut widths, &mut arrange);
3099    h.query.set(query);
3100    h.widths.set(widths);
3101    h.arrange.set(arrange);
3102    h.page.set(0);
3103}
3104
3105/// The keys of rows that stay on a server: there is no list of them to walk.
3106/// Where the filter builder keeps its expression: the column that has one, or else a
3107/// column whose filter it would not replace.
3108/// Every expression the columns filter by, as one: what the filter editor opens.
3109/// Short filter-row text, such as `ana` or `>10`, stays in its column.
3110fn whole_filter(query: &GridQuery) -> String {
3111    let parts: Vec<&str> = query.filters.iter().map(|f| f.trim()).filter(|f| f.contains('[')).collect();
3112    if parts.len() < 2 {
3113        return parts.first().map_or_else(String::new, |f| (*f).to_owned());
3114    }
3115    use ravenrs_criteria::Criteria;
3116    match parts.iter().map(|f| Criteria::parse(f)).collect::<Result<Vec<_>, _>>() {
3117        Ok(all) => all.into_iter().reduce(Criteria::and).map(|c| c.to_string()).unwrap_or_default(),
3118        Err(_) => parts.iter().map(|f| format!("({f})")).collect::<Vec<_>>().join(" And "),
3119    }
3120}
3121
3122/// Writes what the filter editor made in place of every expression the columns had,
3123/// in the column of the first one so it stays where it was.
3124fn set_whole_filter(query: &mut GridQuery, columns: &[GridColumn], text: String) {
3125    let target = builder_column(query, columns);
3126    for f in query.filters.iter_mut().filter(|f| f.contains('[')) {
3127        f.clear();
3128    }
3129    if let Some(c) = target {
3130        query.set_filter(c, text);
3131    }
3132}
3133
3134fn builder_column(query: &GridQuery, columns: &[GridColumn]) -> Option<usize> {
3135    let open = || (0..columns.len()).filter(|&c| columns[c].filterable);
3136    open()
3137        .find(|&c| query.filter(c).contains('['))
3138        .or_else(|| open().find(|&c| query.filter(c).trim().is_empty()))
3139        .or_else(|| open().next())
3140}
3141
3142fn set_details(expanded: &Binding<BTreeSet<usize>>, row: usize, open: bool) {
3143    expanded.update(|set| {
3144        if open {
3145            set.insert(row);
3146        } else {
3147            set.remove(&row);
3148        }
3149    });
3150}
3151
3152fn remote_key_handler(
3153    h: &Handles,
3154    len: usize,
3155    mode: SelectionMode,
3156    details: bool,
3157    edit: Option<(RemoteRows, usize, GridColumn)>,
3158) -> crate::KeyHandler {
3159    let h = h.clone();
3160    Rc::new(move |key, _| {
3161        if len == 0 {
3162            return false;
3163        }
3164        let current = h.focus_row.get().unwrap_or(0).min(len - 1);
3165        let next = match key {
3166            key if edit.is_some()
3167                && (*key == Key::Enter || matches!(key, Key::Other(k) if k.contains("F2"))) =>
3168            {
3169                if let Some((rows, c, column)) = &edit
3170                    && let Some(values) = rows.row(current)
3171                {
3172                    let value = values.get(*c).cloned().unwrap_or_default();
3173                    h.edit_buffer.set(column.display(&value));
3174                    h.editing.set(Some((current, *c)));
3175                }
3176                return true;
3177            }
3178            Key::ArrowLeft | Key::ArrowRight if details => {
3179                set_details(&h.expanded, current, matches!(key, Key::ArrowRight));
3180                return true;
3181            }
3182            Key::ArrowDown => (current + 1).min(len - 1),
3183            Key::ArrowUp => current.saturating_sub(1),
3184            Key::Home => 0,
3185            Key::End => len - 1,
3186            Key::PageDown => (current + 15).min(len - 1),
3187            Key::PageUp => current.saturating_sub(15),
3188            _ => return false,
3189        };
3190        h.focus_row.set(Some(next));
3191        h.reveal.set(Some(next));
3192        if mode == SelectionMode::Single {
3193            h.selection.set(BTreeSet::from([next]));
3194        }
3195        true
3196    })
3197}
3198
3199impl View for DataGrid {
3200    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
3201        let theme = *cx.theme();
3202        let h = self.handles(cx);
3203        // A click that came with letting go of a drag has been and gone.
3204        h.dragged.set(false);
3205        // Someone else changed the arrangement, or it comes first from outside.
3206        if h.original.borrow().is_none() {
3207            *h.original.borrow_mut() = Some(current_layout(&h, &self.columns));
3208        }
3209        if let Some(binding) = &self.layout {
3210            let outside = binding.get();
3211            let changed = h.seen_layout.borrow().as_ref() != Some(&outside);
3212            if changed && outside != GridLayout::default() {
3213                apply_layout(&h, &self.columns, &outside);
3214            }
3215            *h.seen_layout.borrow_mut() = Some(outside);
3216        }
3217        if let Some(remote) = &self.remote {
3218            remote.watch();
3219        }
3220        let rows = self.data.rows();
3221        let query = h.query.get();
3222        let arrangement = h.arrange.get();
3223        let cache: Rc<RefCell<Option<Cached>>> = cx.use_state(|| Rc::new(RefCell::new(None))).get();
3224        let result = if self.remote.is_some() {
3225            let totals = self.remote.as_ref().map(RemoteRows::totals).unwrap_or_default();
3226            Rc::new(QueryResult { totals, ..QueryResult::default() })
3227        } else {
3228            let mut slot = cache.borrow_mut();
3229            match &*slot {
3230                Some(c)
3231                    if c.query == query
3232                        && Rc::ptr_eq(&c.rows, &rows)
3233                        && c.summaries == arrangement.summaries =>
3234                {
3235                    c.result.clone()
3236                }
3237                _ => {
3238                    let _span = tracing::trace_span!("ravenrs::datagrid::query", rows = rows.len()).entered();
3239                    // What each column sums up can have changed since it was declared.
3240                    let mut columns = self.columns.clone();
3241                    for (column, summary) in columns.iter_mut().zip(&arrangement.summaries) {
3242                        column.summary = *summary;
3243                    }
3244                    let result = Rc::new(run_query(&columns, &rows, &query));
3245                    *slot = Some(Cached {
3246                        summaries: arrangement.summaries.clone(),
3247                        query: query.clone(),
3248                        rows: rows.clone(),
3249                        result: result.clone(),
3250                    });
3251                    result
3252                }
3253            }
3254        };
3255        {
3256            // Answers are about these rows and these formulas.
3257            let about = (Rc::as_ptr(&result) as usize, arrangement.formulas.clone());
3258            let mut cache = h.formula_cache.borrow_mut();
3259            if cache.about != about {
3260                cache.about = about;
3261                cache.answers.clear();
3262            }
3263        }
3264        let by_formula = arrangement.formulas.iter().any(Option::is_some);
3265        let ranges = Rc::new(if by_formula { group_ranges(&result) } else { Vec::new() });
3266        let mut widths = h.widths.get();
3267        let visible: Vec<usize> = arrangement.visible(&query);
3268        // The first column takes in the steps of the groups, so its text keeps its room.
3269        if let Some(&first) = visible.first() {
3270            widths[first] += query.group_by.len() as f32 * GROUP_INDENT;
3271        }
3272        let select_width = if self.selection_mode == SelectionMode::Multiple { CHECK_WIDTH } else { 0.0 }
3273            + if self.row_detail.is_some() { DETAIL_ARROW_WIDTH } else { 0.0 };
3274        let fill_seen: Rc<Cell<(f32, f32)>> = cx.use_state(|| Rc::new(Cell::new((0.0, 0.0)))).get();
3275        if let Some(&fill) = visible.iter().find(|&&c| self.columns[c].fill) {
3276            match cx.scroll_viewport() {
3277                Some(view) => {
3278                    let used = select_width
3279                        + visible.iter().map(|&c| widths[c]).sum::<f32>()
3280                        + self.row_actions.len() as f32 * ROW_ACTION_WIDTH;
3281                    widths[fill] += (view.width - used).max(0.0);
3282                    // This build saw the last layout's width. Nothing else asks for a build
3283                    // after a layout that changed it, as when a sidebar closes or the
3284                    // window grows.
3285                    let seen = (view.width, cx.viewport().width);
3286                    if fill_seen.replace(seen) != seen {
3287                        let now = cx.now();
3288                        cx.wake_at(now);
3289                    }
3290                }
3291                // The width comes from the first layout, and nothing else asks for a second build.
3292                None => {
3293                    let now = cx.now();
3294                    cx.wake_at(now);
3295                }
3296            }
3297        }
3298        let total_width = select_width
3299            + visible.iter().map(|&c| widths[c]).sum::<f32>()
3300            + self.row_actions.len() as f32 * ROW_ACTION_WIDTH;
3301        let selected = h.selection.get();
3302        let focus_row = h.focus_row.get();
3303        let editing = h.editing.get();
3304        let offset = cx.scroll_offset();
3305        let mut fixed = Vec::new();
3306        let mut x = select_width;
3307        for &c in &visible {
3308            if arrangement.pinned.get(c).copied().unwrap_or(false) {
3309                fixed.push((c, x));
3310                x += widths[c];
3311            }
3312        }
3313        let last_fixed = fixed.last().map(|(c, _)| *c);
3314        let fixed_width = if fixed.is_empty() { 0.0 } else { x };
3315        let drag = h.drag.get();
3316        let column_facts: Rc<Vec<(String, bool)>> =
3317            Rc::new(self.columns.iter().map(|c| (c.title.clone(), c.groupable)).collect());
3318        let lines: Rc<Vec<f32>> = Rc::new(if self.vertical_lines {
3319            let mut x = select_width;
3320            visible
3321                .iter()
3322                .map(|&c| {
3323                    x += widths[c];
3324                    x - 1.0
3325                })
3326                .collect()
3327        } else {
3328            Vec::new()
3329        });
3330        let shared = Shared {
3331            grid: self,
3332            rows: &rows,
3333            rows_rc: rows.clone(),
3334            result: result.clone(),
3335            visible: &visible,
3336            widths: &widths,
3337            total_width,
3338            select_width,
3339            selected: &selected,
3340            focus_row,
3341            editing,
3342            h: h.clone(),
3343            arrangement: &arrangement,
3344            fixed,
3345            last_fixed,
3346            fixed_width,
3347            offset_x: offset.x,
3348            indent: query.group_by.len() as f32 * GROUP_INDENT,
3349            column_facts: column_facts.clone(),
3350            drop_before: drag.and_then(|d| if let DropTarget::Column(b) = d.target { Some(b) } else { None }),
3351            lines,
3352            ranges,
3353        };
3354
3355        // How many rows there are, and which of them this page shows.
3356        let total = self.remote.as_ref().map_or(result.rows.len(), RemoteRows::len);
3357        let page_size = arrangement.page_size;
3358        let pages = page_size.map_or(1, |size| total.div_ceil(size).max(1));
3359        let page = h.page.get().min(pages - 1);
3360        let base = page_size.map_or(0, |size| page * size);
3361        let len = page_size.map_or(total, |size| size.min(total.saturating_sub(base)));
3362        let shown_page: Rc<Cell<usize>> = cx.use_state(|| Rc::new(Cell::new(0))).get();
3363
3364        let mut sections = Vec::new();
3365        if self.toolbar {
3366            sections.push(self.toolbar_view(cx, &h, &result, &rows, total));
3367        }
3368        if let Some(panel) = self.group_panel_view(cx, &h, &rows, &column_facts) {
3369            sections.push(panel);
3370        }
3371        if let Some(scenarios) = &self.scenarios {
3372            sections.push(self.scenario_dialog(cx, &h, scenarios));
3373        }
3374        // The filter line opens the editor too, so it is there whenever something is filtered.
3375        let filtering = query.filters.iter().any(|f| !f.trim().is_empty());
3376        if self.filter_builder || filtering || h.building_filter.get() {
3377            sections.push(self.filter_dialog(cx, &h, &rows));
3378        }
3379
3380        // Header.
3381        // The last paint's places stay, for whatever opens under a header in this build.
3382        h.zones.borrow_mut().cells.retain(|c, _| visible.contains(c));
3383        let select_fixed = shared.fixed_width > 0.0;
3384        let mut header_cells = Vec::new();
3385        if select_fixed {
3386            header_cells.push(cx.container(
3387                LayoutStyle { width: Some(shared.fixed_width), ..LayoutStyle::leaf() },
3388                &[],
3389                Paint::None,
3390            ));
3391        }
3392        if select_width > 0.0 && self.selection_mode != SelectionMode::Multiple {
3393            header_cells.push(cx.container(
3394                LayoutStyle {
3395                    width: Some(select_width),
3396                    absolute: select_fixed.then_some((0.0, 0.0)),
3397                    ..LayoutStyle::leaf()
3398                },
3399                &[],
3400                Paint::None,
3401            ));
3402        } else if select_width > 0.0 {
3403            let all = !result.matched.is_empty() && result.matched.iter().all(|r| selected.contains(r));
3404            let b = crate::inputs::check_box(cx, &theme, all, false);
3405            let surface = theme.color(Semantic::Surface);
3406            let cell = cx.container(
3407                LayoutStyle {
3408                    width: Some(select_width),
3409                    height: Some(HEADER_HEIGHT),
3410                    align: Align::Center,
3411                    justify: Justify::Center,
3412                    absolute: select_fixed.then_some((0.0, 0.0)),
3413                    ..LayoutStyle::stack(Axis::Horizontal)
3414                },
3415                &[b],
3416                if select_fixed { Paint::Quad(QuadPaint::new(surface)) } else { Paint::None },
3417            );
3418            if select_fixed {
3419                cx.translate(cell, Point::new(offset.x, 0.0));
3420            }
3421            let (sel, matched) = (h.selection.clone(), result.matched.clone());
3422            cx.on_click(
3423                cell,
3424                Rc::new(move || {
3425                    sel.set(if all { BTreeSet::new() } else { matched.iter().copied().collect() })
3426                }),
3427            );
3428            cx.cursor(cell, CursorIcon::Pointer);
3429            let name = cx.labels().grid_select_row.clone();
3430            cx.semantics(cell, Semantics::new(Role::CheckBox).name(name).toggled(all));
3431            header_cells.push(cell);
3432        }
3433        let flowing: Vec<usize> = visible.iter().copied().filter(|c| shared.fixed_x(*c).is_none()).collect();
3434        for &c in flowing.iter().chain(shared.fixed.iter().map(|(c, _)| c)) {
3435            header_cells.push(cx.keyed_child(c as u64, &HeaderCell { column: c, shared: &shared }));
3436        }
3437        let zones = h.zones.clone();
3438        let band_fill = theme.color(Semantic::Control).faded(0.6);
3439        let header = Self::band(cx, header_cells, &shared, offset.x, band_fill, false, HEADER_HEIGHT);
3440        let header_zone = cx.container(
3441            LayoutStyle {
3442                align: Align::Stretch,
3443                align_self: Some(Align::Stretch),
3444                ..LayoutStyle::stack(Axis::Vertical)
3445            },
3446            &[header],
3447            Paint::Custom(Rc::new(move |f: Rect, _| zones.borrow_mut().header = f)),
3448        );
3449        sections.push(header_zone);
3450
3451        if self.filter_row {
3452            let mut cells = Vec::new();
3453            if select_fixed {
3454                cells.push(cx.container(
3455                    LayoutStyle { width: Some(shared.fixed_width), ..LayoutStyle::leaf() },
3456                    &[],
3457                    Paint::None,
3458                ));
3459            } else if select_width > 0.0 {
3460                cells.push(cx.container(
3461                    LayoutStyle { width: Some(select_width), ..LayoutStyle::leaf() },
3462                    &[],
3463                    Paint::None,
3464                ));
3465            }
3466            for &c in flowing.iter().chain(shared.fixed.iter().map(|(c, _)| c)) {
3467                cells.push(cx.keyed_child(1000 + c as u64, &FilterCell { column: c, shared: &shared }));
3468            }
3469            sections.push(Self::band(cx, cells, &shared, offset.x, Color::TRANSPARENT, false, HEADER_HEIGHT));
3470        }
3471
3472        // Body: only the rows in (and just around) the viewport are built.
3473        let rh = self.row_height;
3474        let viewport_h =
3475            cx.scroll_viewport().map_or(self.height.unwrap_or(cx.viewport().height), |s| s.height);
3476        let viewport_w = cx.scroll_viewport().map_or(cx.viewport().width, |s| s.width);
3477        let expanded = if self.row_detail.is_some() { h.expanded.get() } else { BTreeSet::new() };
3478        let tops = RowTops {
3479            row_height: rh,
3480            detail_height: self.detail_height,
3481            open: if expanded.is_empty() {
3482                Vec::new()
3483            } else {
3484                (0..len)
3485                    .filter(|&i| match result.rows.get(base + i) {
3486                        Some(ViewRow::Data(r)) => expanded.contains(r),
3487                        Some(_) => false,
3488                        None => self.remote.is_some() && expanded.contains(&(base + i)),
3489                    })
3490                    .collect()
3491            },
3492        };
3493        if shown_page.replace(page) != page {
3494            cx.set_scroll_offset(Point::new(offset.x, 0.0));
3495        }
3496        if let Some(i) = h.reveal.take() {
3497            let top = tops.top(i.saturating_sub(base));
3498            let y = if top < offset.y {
3499                Some(top)
3500            } else if top + rh > offset.y + viewport_h {
3501                Some(top + rh - viewport_h)
3502            } else {
3503                None
3504            };
3505            if let Some(y) = y {
3506                cx.set_scroll_offset(Point::new(offset.x, y.max(0.0)));
3507            }
3508        }
3509        let overscan = 6;
3510        let first = tops.row_at(offset.y, len).saturating_sub(overscan).min(len);
3511        let last = (tops.row_at(offset.y + viewport_h, len) + 1 + overscan).min(len);
3512        if let Some(remote) = &self.remote {
3513            let (from, to) = (base + first, base + last.max(first + 1));
3514            let now = cx.now();
3515            if let Some(when) = remote.want(from, to, &query, now) {
3516                cx.wake_at(when);
3517            }
3518        }
3519        let mut body_rows = Vec::with_capacity(last - first + 2);
3520        body_rows.push(cx.container(
3521            LayoutStyle { height: Some(tops.top(first)), width: Some(total_width), ..LayoutStyle::leaf() },
3522            &[],
3523            Paint::None,
3524        ));
3525        // Only with groups is the count not just the place in the list.
3526        let mut since_group = (base + first)
3527            - result.rows[..(base + first).min(result.rows.len())]
3528                .iter()
3529                .rposition(|row| matches!(row, ViewRow::Group { .. }))
3530                .map_or(0, |group| group + 1);
3531        for i in (first..last).map(|i| base + i) {
3532            if let Some(remote) = &self.remote {
3533                let node = match remote.row(i) {
3534                    Some(values) => cx.keyed_child(
3535                        i as u64,
3536                        &DataRow { index: i, even: i % 2 == 1, row: i, values, shared: &shared },
3537                    ),
3538                    None => cx.keyed_child(i as u64 | (1 << 61), &WaitingRow { shared: &shared }),
3539                };
3540                body_rows.push(node);
3541                if expanded.contains(&i)
3542                    && let Some(values) = remote.row(i)
3543                {
3544                    let detail = DetailRow { row: i, values: &values, shared: &shared, width: viewport_w };
3545                    body_rows.push(cx.keyed_child(i as u64 | (1 << 60), &detail));
3546                }
3547                continue;
3548            }
3549            let node = match &result.rows[i] {
3550                ViewRow::Data(r) => cx.keyed_child(
3551                    *r as u64,
3552                    &DataRow {
3553                        index: i,
3554                        even: since_group % 2 == 1,
3555                        row: *r,
3556                        values: rows[*r].clone(),
3557                        shared: &shared,
3558                    },
3559                ),
3560                ViewRow::Group { key, .. } => {
3561                    since_group = 0;
3562                    cx.keyed_child(hash_of(key) | (1 << 62), &GroupRow { index: i, shared: &shared })
3563                }
3564            };
3565            if matches!(result.rows[i], ViewRow::Data(_)) {
3566                since_group += 1;
3567            }
3568            body_rows.push(node);
3569            if let ViewRow::Data(r) = &result.rows[i]
3570                && expanded.contains(r)
3571            {
3572                let detail = DetailRow { row: *r, values: &rows[*r], shared: &shared, width: viewport_w };
3573                body_rows.push(cx.keyed_child(*r as u64 | (1 << 60), &detail));
3574            }
3575        }
3576        body_rows.push(cx.container(
3577            LayoutStyle {
3578                height: Some(tops.top(len) - tops.top(last)),
3579                width: Some(total_width),
3580                ..LayoutStyle::leaf()
3581            },
3582            &[],
3583            Paint::None,
3584        ));
3585        let failure = self.remote.as_ref().and_then(RemoteRows::error);
3586        if let Some(message) = &failure {
3587            let text = util::label(cx, message, TextStyle::Body, None, theme.color(Semantic::Danger));
3588            let remote = self.remote.clone();
3589            let again = cx.child(
3590                &Button::new(cx.labels().grid_try_again.clone())
3591                    .style(ButtonStyle::Secondary)
3592                    .size(ButtonSize::Small)
3593                    .on_click(move || {
3594                        if let Some(remote) = &remote {
3595                            remote.retry();
3596                        }
3597                    }),
3598            );
3599            body_rows.insert(
3600                0,
3601                cx.container(
3602                    LayoutStyle {
3603                        padding: Insets::uniform(16.0),
3604                        spacing: 12.0,
3605                        align: Align::Center,
3606                        ..LayoutStyle::stack(Axis::Horizontal)
3607                    },
3608                    &[text, again],
3609                    Paint::None,
3610                ),
3611            );
3612        } else if total == 0 {
3613            let empty = self
3614                .empty_text
3615                .clone()
3616                .filter(|_| self.remote.is_none() && self.data.rows().is_empty())
3617                .unwrap_or_else(|| cx.labels().grid_no_rows.clone());
3618            let msg = util::label(cx, &empty, TextStyle::Body, None, theme.color(Semantic::InkMuted));
3619            body_rows.push(cx.container(
3620                LayoutStyle { padding: Insets::uniform(24.0), ..LayoutStyle::stack(Axis::Horizontal) },
3621                &[msg],
3622                Paint::None,
3623            ));
3624        }
3625        let content = cx.container(
3626            LayoutStyle { width: Some(total_width), ..LayoutStyle::stack(Axis::Vertical) },
3627            &body_rows,
3628            Paint::None,
3629        );
3630        let body = cx.container(
3631            LayoutStyle {
3632                grow: 1.0,
3633                min_height: Some(0.0),
3634                overflow_hidden: true,
3635                align_self: Some(Align::Stretch),
3636                ..LayoutStyle::stack(Axis::Vertical)
3637            },
3638            &[content],
3639            Paint::None,
3640        );
3641        cx.scrollable_virtual(body, true);
3642        cx.semantics(
3643            body,
3644            Semantics::new(Role::Grid)
3645                .name(cx.labels().grid_name.clone())
3646                .description(format!("{total} {}", cx.labels().grid_rows)),
3647        );
3648        cx.focusable(body, 0.0);
3649        let clipboard = cx.clipboard();
3650        let keys = if self.remote.is_some() {
3651            {
3652                // Only an app that answers for edits can change rows on a server.
3653                let edit = self.remote.clone().filter(|_| self.on_cell_edit.is_some()).and_then(|rows| {
3654                    visible
3655                        .iter()
3656                        .find(|&&c| self.columns[c].editable)
3657                        .map(|&c| (rows, c, self.columns[c].clone()))
3658                });
3659                remote_key_handler(&h, total, self.selection_mode, self.row_detail.is_some(), edit)
3660            }
3661        } else {
3662            key_handler(&shared, self.selection_mode, clipboard)
3663        };
3664        cx.on_key(body, keys);
3665        sections.push(body);
3666
3667        // What is being filtered, at a glance, with a way out.
3668        let filters: Vec<String> = (0..self.columns.len())
3669            .filter(|c| !query.filter(*c).trim().is_empty())
3670            .map(|c| {
3671                let text = query.filter(c);
3672                if text.contains('[') {
3673                    text.to_owned()
3674                } else {
3675                    format!("[{}]: {text}", self.columns[c].title)
3676                }
3677            })
3678            .collect();
3679        if !filters.is_empty() {
3680            let labels = cx.labels();
3681            let muted = theme.color(Semantic::InkMuted);
3682            let icon = util::icon(cx, IconName::Filter, 14.0, theme.color(Semantic::Accent));
3683            let text = format!("{}: {}", labels.grid_filter_label, filters.join(" ยท "));
3684            let label = util::label(cx, &text, TextStyle::Callout, None, muted);
3685            let open = h.building_filter.clone();
3686            let edit = cx.child(
3687                &Button::new(labels.grid_edit_filter.clone())
3688                    .style(ButtonStyle::Ghost)
3689                    .size(ButtonSize::Small)
3690                    .icon(IconName::Pencil)
3691                    .on_click(move || open.set(true)),
3692            );
3693            let q = h.query.clone();
3694            let clear = cx.child(
3695                &Button::new(labels.grid_clear_filter.clone())
3696                    .style(ButtonStyle::Ghost)
3697                    .size(ButtonSize::Small)
3698                    .on_click(move || q.update(|q| q.filters.clear())),
3699            );
3700            let line = theme.color(Semantic::Border);
3701            sections.push(cx.container(
3702                LayoutStyle {
3703                    padding: Insets::symmetric(4.0, 12.0),
3704                    spacing: 8.0,
3705                    align: Align::Center,
3706                    align_self: Some(Align::Stretch),
3707                    ..LayoutStyle::stack(Axis::Horizontal)
3708                },
3709                &[icon, label, edit, clear],
3710                Paint::Custom(Rc::new(move |f: Rect, scene| {
3711                    scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), f.width(), 1.0), line));
3712                })),
3713            ));
3714        }
3715
3716        // Footer totals.
3717        if self.footer
3718            && (arrangement.summaries.iter().any(Option::is_some)
3719                || arrangement.formulas.iter().any(Option::is_some))
3720        {
3721            let mut cells = Vec::new();
3722            if select_fixed {
3723                cells.push(cx.container(
3724                    LayoutStyle { width: Some(shared.fixed_width), ..LayoutStyle::leaf() },
3725                    &[],
3726                    Paint::None,
3727                ));
3728            } else if select_width > 0.0 {
3729                cells.push(cx.container(
3730                    LayoutStyle { width: Some(select_width), ..LayoutStyle::leaf() },
3731                    &[],
3732                    Paint::None,
3733                ));
3734            }
3735            for &c in flowing.iter().chain(shared.fixed.iter().map(|(c, _)| c)) {
3736                cells.push(cx.keyed_child(2000 + c as u64, &FooterCell { column: c, shared: &shared }));
3737            }
3738            sections.push(Self::band(cx, cells, &shared, offset.x, band_fill, true, FOOTER_HEIGHT));
3739        }
3740
3741        // The pager.
3742        if page_size.is_some() {
3743            let labels = cx.labels();
3744            let muted = theme.color(Semantic::InkMuted);
3745            let fmt = crate::grid::model::NumberFormat::default();
3746            let range = if total == 0 {
3747                String::new()
3748            } else {
3749                format!(
3750                    "{}โ€“{} {} {}",
3751                    fmt.format((base + 1) as f64),
3752                    fmt.format((base + len) as f64),
3753                    labels.grid_of,
3754                    fmt.format(total as f64)
3755                )
3756            };
3757            let (read, write, reset) = (h.arrange.clone(), h.arrange.clone(), h.page.clone());
3758            let chosen = Binding::new(
3759                move || {
3760                    let now = read.peek().page_size.unwrap_or(PAGE_SIZES[0]);
3761                    PAGE_SIZES.iter().position(|s| *s == now).unwrap_or(0)
3762                },
3763                move |i: usize| {
3764                    write.update(|a| a.page_size = PAGE_SIZES.get(i).copied());
3765                    reset.set(0);
3766                },
3767            );
3768            let (per_page, picker, status, pager, gap) = (
3769                util::label(cx, &labels.grid_rows_per_page, TextStyle::Callout, None, muted),
3770                cx.child(
3771                    &Select::new(chosen, PAGE_SIZES.map(|s| s.to_string()))
3772                        .label(labels.grid_rows_per_page.clone())
3773                        .width(84.0),
3774                ),
3775                util::label(cx, &range, TextStyle::Callout, None, muted),
3776                cx.child(&Pagination::new(h.page.binding(), pages).compact().labels(
3777                    labels.grid_previous.clone(),
3778                    labels.grid_next.clone(),
3779                    labels.grid_page.clone(),
3780                )),
3781                cx.child(&Spacer::new()),
3782            );
3783            let line = theme.color(Semantic::Border);
3784            sections.push(cx.container(
3785                LayoutStyle {
3786                    padding: Insets::symmetric(6.0, 12.0),
3787                    spacing: 8.0,
3788                    align: Align::Center,
3789                    align_self: Some(Align::Stretch),
3790                    ..LayoutStyle::stack(Axis::Horizontal)
3791                },
3792                &[status, gap, per_page, picker, pager],
3793                Paint::Custom(Rc::new(move |f: Rect, scene| {
3794                    scene.push_quad(Quad::new(Rect::new(f.x(), f.y(), f.width(), 1.0), line));
3795                })),
3796            ));
3797        }
3798
3799        let border = theme.color(Semantic::Border);
3800        let surface = theme.color(Semantic::Surface);
3801        let root = cx.container(
3802            LayoutStyle {
3803                height: self.height,
3804                grow: if self.height.is_none() { 1.0 } else { 0.0 },
3805                min_height: Some(200.0),
3806                align: Align::Stretch,
3807                align_self: Some(Align::Stretch),
3808                ..LayoutStyle::stack(Axis::Vertical)
3809            },
3810            &sections,
3811            Paint::Quad(QuadPaint {
3812                corner_radius: theme.radii.lg,
3813                border: Some(ravenrs_core::Border { width: 1.0, color: border }),
3814                ..QuadPaint::new(surface)
3815            }),
3816        );
3817        cx.clip(root);
3818
3819        // A column's summary is edited under its header.
3820        if let Some(c) = h.summary_edit.get().filter(|c| *c < self.columns.len()) {
3821            let close: Rc<dyn Fn()> = {
3822                let edit = h.summary_edit.clone();
3823                Rc::new(move || edit.set(None))
3824            };
3825            let at =
3826                h.zones.borrow().cells.get(&c).map_or_else(
3827                    || Point::new(offset.x + 40.0, 120.0),
3828                    |r| Point::new(r.x(), r.max_y() + 4.0),
3829                );
3830            let editor = SummaryEditor {
3831                column: c,
3832                columns: self.columns.clone(),
3833                rows: rows.clone(),
3834                matched: result.clone(),
3835                arrange: h.arrange.clone(),
3836                draft: h.summary_draft.clone(),
3837                close: close.clone(),
3838            };
3839            cx.overlay(root, Placement::At(at), &PanelOf(&editor), Some(close));
3840        }
3841
3842        // What is being dragged follows the pointer, over everything.
3843        if let Some(d) = drag {
3844            let column = match d.source {
3845                DragSource::Column(c) | DragSource::Chip(c) => c,
3846            };
3847            let ghost = DragGhost {
3848                title: self.columns.get(column).map_or_else(String::new, |c| c.title.clone()),
3849                grouping: matches!(d.target, DropTarget::Group(_)),
3850                ungrouping: d.target == DropTarget::Ungroup,
3851            };
3852            let cancel = h.drag.clone();
3853            cx.overlay(
3854                root,
3855                Placement::At(Point::new(d.at.x + 12.0, d.at.y + 8.0)),
3856                &ghost,
3857                Some(Rc::new(move || cancel.set(None))),
3858            );
3859        }
3860
3861        // What was arranged, for whoever keeps it.
3862        if let Some(binding) = &self.layout {
3863            let now = current_layout(&h, &self.columns);
3864            let seen = h.seen_layout.borrow().clone();
3865            if seen.as_ref() != Some(&now) {
3866                *h.seen_layout.borrow_mut() = Some(now.clone());
3867                binding.set(now);
3868            }
3869        }
3870        root
3871    }
3872}
3873
3874#[cfg(test)]
3875mod tests {
3876    use super::*;
3877
3878    #[test]
3879    fn rows_stand_lower_after_open_details() {
3880        let tops = RowTops { row_height: 10.0, detail_height: 100.0, open: vec![1, 3] };
3881        assert_eq!(tops.top(0), 0.0);
3882        assert_eq!(tops.top(1), 10.0);
3883        assert_eq!(tops.top(2), 120.0, "row 1 and its details");
3884        assert_eq!(tops.top(4), 240.0);
3885        assert_eq!(tops.row_at(0.0, 6), 0);
3886        assert_eq!(tops.row_at(15.0, 6), 1);
3887        assert_eq!(tops.row_at(119.0, 6), 1, "inside the details of row 1");
3888        assert_eq!(tops.row_at(120.0, 6), 2);
3889        assert_eq!(tops.row_at(10_000.0, 6), 6, "past the end");
3890    }
3891
3892    #[test]
3893    fn the_filter_builder_does_not_take_over_a_filter_in_use() {
3894        let columns = [GridColumn::text("A").filterable(false), GridColumn::text("B"), GridColumn::text("C")];
3895        let mut query = GridQuery::default();
3896        assert_eq!(builder_column(&query, &columns), Some(1), "the first that can filter");
3897        query.set_filter(1, "ana".to_owned());
3898        assert_eq!(builder_column(&query, &columns), Some(2), "not over what someone typed");
3899        query.set_filter(2, "bia".to_owned());
3900        assert_eq!(builder_column(&query, &columns), Some(1), "with no room, the first");
3901        query.set_filter(2, "[B] = 'x'".to_owned());
3902        assert_eq!(builder_column(&query, &columns), Some(2), "its own expression, wherever it is");
3903    }
3904
3905    #[test]
3906    fn the_editor_opens_every_expression_as_one() {
3907        let columns = [GridColumn::text("A"), GridColumn::text("B"), GridColumn::text("C")];
3908        let mut query = GridQuery::default();
3909        assert_eq!(whole_filter(&query), "");
3910        query.set_filter(0, "ana".to_owned());
3911        query.set_filter(1, "[B] In ('x', 'y')".to_owned());
3912        assert_eq!(whole_filter(&query), "[B] In ('x', 'y')", "typed text stays in its column");
3913        query.set_filter(2, "[C] = 'z' Or [C] = 'w'".to_owned());
3914        assert_eq!(whole_filter(&query), "[B] In ('x', 'y') And ([C] = 'z' Or [C] = 'w')");
3915
3916        set_whole_filter(&mut query, &columns, "[C] = 'z'".to_owned());
3917        assert_eq!(query.filter(0), "ana");
3918        assert_eq!(query.filter(1), "[C] = 'z'", "where the first expression was");
3919        assert_eq!(query.filter(2), "");
3920        assert_eq!(whole_filter(&query), "[C] = 'z'");
3921        set_whole_filter(&mut query, &columns, String::new());
3922        assert_eq!(whole_filter(&query), "");
3923        assert_eq!(query.filter(0), "ana");
3924    }
3925
3926    #[test]
3927    fn with_nothing_open_a_row_is_its_number_of_heights_down() {
3928        let tops = RowTops { row_height: 36.0, detail_height: 240.0, open: vec![] };
3929        for row in [0, 1, 5, 100_000] {
3930            assert_eq!(tops.top(row), row as f32 * 36.0);
3931            assert_eq!(tops.row_at(row as f32 * 36.0 + 1.0, 200_000), row);
3932        }
3933    }
3934}