Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/grid-guide.md

facett-grid — guide

Class: guide · audience: a techie building a screen with facett-grid or driving one from a test/host. The what is in grid-design.md; the zero-knowledge walkthrough is grid-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:

  1. Build from an Arrow RecordBatch (f1).
  2. Let the grid scroll — virtualization is automatic (f2).
  3. Wire header clicks to sort, or drive Msg::ClickHeader from a test (f3).
  4. Read/drive selection for copy or a custom toolbar action (f4).
  5. Attach conditional formatting / progress / sparkline to the columns that need it (f5).
  6. 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.