Expand description
oriel-iced — a virtualized, batteries-included data grid for
iced 0.14.
Sort, filter, live transactional ingest with tail-follow, selection and keyboard navigation, resizable/auto-sizing columns, footers with aggregates, row animations, themes (CSS-verified ports of ag-grid Quartz/Alpine/Balham, Material, Spreadsheet, Bootstrap), and smooth scrolling over hundreds of thousands of rows — only the visible slice is ever materialized.
The heavy thinking lives in oriel_core (headless: the data seam, the
column model, virtualization math, view state); this crate renders it
with iced and owns all pixel concerns. Run the full tour with
cargo run --example showcase --release.
§Quickstart
A complete application: 100 000 rows, sortable/filterable/selectable, with resizable auto-sizing columns — the grid’s defaults.
use iced::{Element, Task};
use oriel_core::columns::ColumnId;
use oriel_core::data::VecStore;
use oriel_core::query::FilterValue;
use oriel_iced::column::TextColumn;
use oriel_iced::grid::{self, BoxedColumn, VirtualGrid};
#[derive(Clone, Debug)]
struct Person {
id: u64,
name: String,
}
const ID_COL: ColumnId = ColumnId(1);
const NAME_COL: ColumnId = ColumnId(2);
type Store = VecStore<Person, u64, fn(&Person) -> u64>;
struct App {
grid: VirtualGrid<Store>,
}
#[derive(Debug, Clone)]
enum Msg {
Grid(grid::Message<Person>),
}
fn boot() -> (App, Task<Msg>) {
fn key(p: &Person) -> u64 {
p.id
}
let rows = (0..100_000u64)
.map(|id| Person { id, name: format!("Person {id}") })
.collect();
// The field resolver makes header-click sorting and filters work.
let store = VecStore::new(rows, key as fn(&Person) -> u64).with_fields(|p, col| {
match col {
ID_COL => Some(FilterValue::Int(p.id as i64)),
NAME_COL => Some(FilterValue::Text(p.name.clone())),
_ => None,
}
});
let columns: Vec<BoxedColumn<Person>> = vec![
Box::new(TextColumn::new(ID_COL, "#", 70.0, |p: &Person| p.id.to_string()).numeric()),
Box::new(TextColumn::new(NAME_COL, "Name", 260.0, |p: &Person| p.name.clone())),
];
let (grid, init) = VirtualGrid::new(store, columns);
(App { grid }, init.map(Msg::Grid))
}
fn update(app: &mut App, msg: Msg) -> Task<Msg> {
match msg {
Msg::Grid(m) => app.grid.update(m).map(Msg::Grid),
}
}
fn view(app: &App) -> Element<'_, Msg> {
app.grid.view().map(Msg::Grid)
}
fn main() -> iced::Result {
iced::application(boot, update, view).run()
}§The wiring contract
The grid is a value in your model, not a widget with hidden state. Three obligations, all visible in the quickstart:
- Forward its messages: everything the grid emits arrives as your
wrapper around
grid::Message; hand it toVirtualGrid::update. - Run the returned tasks:
update— and every imperative method (transact,set_spec,set_follow, …) — returns aniced::Task; return it from yourupdate(batch andmapas needed). Dropping a task drops a fetch or a scroll command. - Map the view:
VirtualGrid::viewrenders the whole grid (header, body, footer, scrollbars).
§Where do I…?
| I want to… | Reach for |
|---|---|
| feed live data (append/update/remove) | grid::VirtualGrid::transact |
| pin the view to the newest rows | grid::VirtualGrid::set_follow (auto-freezes when the user scrolls up) |
| sort / filter programmatically | grid::VirtualGrid::set_spec + grid::VirtualGrid::spec |
| wire keyboard navigation | grid::VirtualGrid::focus_next / focus_prev / page_up / page_down — bind them to any keys |
| read / drive selection | grid::VirtualGrid::selection / selection_mut |
| persist and restore the scroll position | grid::VirtualGrid::scroll_anchor / scroll_to_anchor (row-space: stable under churn) |
| restyle rows/cells/bands | the set_*_style hooks on grid::VirtualGrid, wrapping the public default_* fns |
| apply a whole look | theme::GridTheme presets (apply) |
| render a custom cell (chips, pills, sparklines) | implement oriel_core::columns::Column — see column for the provided renderers |
| put my data behind an async boundary | implement oriel_core::data::DataSource |
| fit columns to content | grip double-click does it; grid::VirtualGrid::auto_size_all for everything |
§Keyboard
Navigation is exposed as actions, not bindings — bind any keys (or
gamepad, or vim chords) to focus_next,
focus_prev,
page_up /
page_down, and
set_follow /
scroll_to_row for End/Home:
fn subscription(_app: &App) -> Subscription<Msg> {
iced::keyboard::listen().map(Msg::Key)
}
fn update(app: &mut App, msg: Msg) -> iced::Task<Msg> {
use iced::keyboard::key::{Key, Named};
match msg {
Msg::Grid(m) => app.grid.update(m).map(Msg::Grid),
Msg::Key(iced::keyboard::Event::KeyPressed { key, .. }) => match key {
Key::Named(Named::ArrowDown) => app.grid.focus_next().map(Msg::Grid),
Key::Named(Named::ArrowUp) => app.grid.focus_prev().map(Msg::Grid),
Key::Named(Named::PageDown) => app.grid.page_down().map(Msg::Grid),
Key::Named(Named::PageUp) => app.grid.page_up().map(Msg::Grid),
_ => iced::Task::none(),
},
Msg::Key(_) => iced::Task::none(),
}
}Gotcha worth knowing: iced::keyboard::listen only delivers events no
widget consumed, and a focused text_input consumes some keys
(Home/End/Delete/arrows-left-right) while letting others through
(arrows-up-down, PageUp/Down) — scope your bindings if the two fight.
§Persisting a session
Everything worth restoring is exposed as a holdable value with a read/write pair:
| State | read | restore |
|---|---|---|
| column widths | column_layout (placements()) | column_layout_mut + set_width (doc example there) |
| sort + filter | spec (serde-ready with oriel-core’s serde feature) | set_spec |
| scroll position | scroll_anchor (row-space — stable under churn) | scroll_to_anchor |
| selection | selection (iterate the RowIds) | selection_mut |
§Testing your integration
The grid is a plain value — no window or renderer needed for state-level
tests: drive update with synthesized
messages and assert on the exposed state.
let _ = grid.update(Message::RowPressed { index: 3 });
assert!(grid.selection().contains(&3));
let _ = grid.focus_next();
assert!(grid.selection().contains(&4)); // nav single-selects as it movesFor full interaction tests (clicks, wheel, lifecycle) the repository’s
test suite drives the real iced_runtime::UserInterface with the
tiny-skia renderer — headless, no GPU; its tests/common/mod.rs is the
copyable harness.
§Scale
Materialized widgets are a function of the viewport, never the row
count. The shipped grid addresses scroll space in flat f32 pixels
(right for resident data): sub-pixel-exact while
rows × row_height < 2^24 px — ~670 000 rows at the default height,
~350 000 at the tallest theme. Beyond that envelope sits the core’s
row-space anchor path (exact to usize::MAX), held open for an
out-of-core integration; a standing core test guards the boundary.
§Modules
grid—VirtualGrid, the widget itself: its messages, knobs, hooks, and public style defaults.column— provided column renderers (TextColumn,BarColumn) and the header font.theme—GridTheme: ready-made looks with documented provenance.arrow,measure,resize,scrollbar,slide— the grid’s hand-rolled widgets (sort arrow, auto-size measurer, resize grips, min-thumb scrollbar, row animations). Public for reuse; most integrators never touch them.
§Versioning
iced 0.14 is pre-1.0 and its Widget API moves between minor versions;
this crate pins the version it is verified against. Interaction
behaviors that depend on upstream internals are guarded by canary tests
in the repository, re-checked on every bump.
Modules§
- arrow
- The sort arrow — ag-grid’s shape, drawn from quads: a straight stem under a sharp chevron head (two 45° strokes meeting at the apex — not a filled triangle, not curved), in the ambient text color.
- column
- Provided column renderers — public examples of the
Columnrender seam plus one bespoke rich renderer (a mini bar + label). Each satisfies the same core trait; a user who outgrows them drops to the raw trait and loses nothing. - grid
- The grid widget —
VirtualGrid, everything it emits and accepts, and its public style defaults. - measure
- A phantom measurer — the auto-size (fit-to-content) mechanism.
- resize
- A column resize handle — a low-level
Widget(drop toWidgetonly where composition can’t, here becausemouse_areacannot capture a drag once the cursor leaves the thin handle). - scrollbar
- The grid’s own vertical scrollbar — because iced’s cannot be told a minimum thumb size.
- slide
- The row animations (ag-grid’s
animateRowspair): - theme
- Ready-made grid themes — faithful ports of the looks users already know.