facett-grid — guide
Class:
guide· audience: a techie building a screen withfacett-gridor driving one from a test/host. The what is ingrid-design.md; the zero-knowledge walkthrough isgrid-for-idiots.md. This file is the workflow.
f0 — zero to a driven grid (the whole workflow)
use facett_grid::{Grid, CondFmt, Density};
let grid = Grid::from_record_batch("vessels", &batch)
.pin_columns(2) // freeze the 2 leading columns
.with_totals(true) // sticky Σ footer for numeric cols
.with_density(Density::Comfortable)
.with_cond_fmt(3, CondFmt::ColorScale) // column 3 → heat
.with_progress(4) // column 4 → in-cell progress bar
.with_sparkline(5); // column 5 → sparkline
Drop it into a deck — FacetDeck::new(vec![Box::new(grid)]) — and the palette picker, Ctrl-+/-/0 zoom, and Ctrl-C copy are routed for you. From there:
- Build from an Arrow
RecordBatch(f1). - Let the grid scroll — virtualization is automatic (
f2). - Wire header clicks to sort, or drive
Msg::ClickHeaderfrom a test (f3). - Read/drive selection for copy or a custom toolbar action (
f4). - Attach conditional formatting / progress / sparkline to the columns that need it (
f5). - Test everything headlessly through the Elm seam, and opt into inline editing if the host needs it (
f6).
The live reference is facett-grid/examples/grid_demo.rs — run it with cargo run -p facett-grid --example grid_demo.
f1 — ingestion
The common path is straight from Arrow:
let grid = facett_grid::Grid::from_record_batch("repos", &batch);
// or, when you already hold an Arc<RecordBatch> (skips a buffer clone):
let grid = facett_grid::Grid::from_record_batch_arc("repos", arc_batch);
If your source isn't Arrow, build a GridData directly from typed columns and use Grid::new:
use facett_grid::{Grid, GridData};
use facett_grid::data::Column;
let data = GridData::new(vec![
("id".into(), Column::Int { v: vec![1, 2, 3], nulls: vec![false; 3] }),
("name".into(), Column::Text { v: vec!["a".into(), "b".into(), "c".into()], nulls: vec![false; 3] }),
]);
let grid = Grid::new("my table", data);
Column widths auto-fit from the header + a row sample at build time; low-cardinality text columns (≤12 distinct values) are auto-promoted to enum chips — no flag needed.
f2 — virtualization in a test
row_window_at is the same call the paint path uses, exposed so a test can assert virtualization stays cheap regardless of row count:
let win = grid.row_window_at(scroll_y, viewport_h); // O(visible), not O(total)
assert!(win.count < 64); // a few dozen rows fill any real viewport
See facett-grid/tests/perf.rs for the full 10k-vs-10M-row timing assertion.
f3 — sorting
grid.click_header(2, false); // sort by column 2 (replaces any prior sort)
grid.click_header(5, true); // shift-click: add column 5 as a secondary key
grid.clear_sort(); // back to source order
Re-clicking the sole active column flips its direction. Sorting never moves the underlying data — grid.order() is the current visual→source permutation, useful when you need to map a visual row back to its source row yourself.
f4 — selection & clipboard
grid.select_cell(r, c); // visual-coord click
grid.extend_selection(r2, c2); // shift-drag to (r2, c2)
For programmatic/headless selection, drive the Msgs directly — they address rows by their stable source id, so a selection made before a sort stays correct after one:
use facett_grid::Msg;
grid.update(Msg::SelectCell { row: source_row_id, col: 1 });
grid.update(Msg::ExtendTo { row: other_source_row_id, col: 3 });
A deck routes Ctrl-C to Facet::copy(), which returns the selection (or the whole table) as TSV. If you need the selection as typed Arrow arrays instead of text (e.g. to hand off to another Arrow consumer with zero copies), call grid.copy_columns() — it only returns Some when the grid was built from a RecordBatch.
f5 — conditional formatting & type-aware cells
Attach formatting at build time (see f0); the effect is column-scoped and automatic once numeric ranges are known (computed once, at construction). No per-cell wiring is needed — CellKind detection (int/float/text/enum/bool/date) happens during ingestion (f1) and drives both alignment and which formatting rules can apply.
f6 — the Elm seam & inline editing
Every gesture the canvas performs is also a Msg you can feed headlessly:
use facett_core::harness;
use facett_grid::Msg;
let model = harness::snapshot(&mut grid, [
Msg::ClickHeader { col: 2, additive: false },
Msg::SelectCell { row: 0, col: 0 },
]);
assert_eq!(model.sort.keys.len(), 1);
// Assert it actually paints:
assert!(harness::headless_render(&mut grid).drew());
state_json() (via the Facet trait) is the same shape a robot/CLI driver reads: rows, cols, visible_rows, columns, kinds, sort, selection, filter, editing, changes.
To make a grid editable:
let mut grid = Grid::from_record_batch("t", &batch).with_editable();
grid.select_cell(0, 0);
grid.begin_edit(); // loads the cell into the edit buffer
grid.update(Msg::SetEditBuffer("42".into()));
let change = grid.commit_edit(); // Some(GridChange) on valid input
for c in grid.take_changes() {
// persist c.row / c.col / c.new_value_str upstream
}
Invalid input (e.g. non-numeric text into an Int column) leaves edit.valid = false and keeps the editor open instead of committing — check grid.edit_state().valid (or state_json()["editing"]["valid"]) before assuming a commit landed.
Look & feel
The grid paints entirely through the active facett_core::Theme — zebra stripes, hover row/column highlight, selection accent, and the conditional-format ramps all derive from it. Switching themes on the deck (the palette picker) re-themes the grid with no extra wiring.