Skip to main content

Crate oriel_iced

Crate oriel_iced 

Source
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:

  1. Forward its messages: everything the grid emits arrives as your wrapper around grid::Message; hand it to VirtualGrid::update.
  2. Run the returned tasks: update — and every imperative method (transact, set_spec, set_follow, …) — returns an iced::Task; return it from your update (batch and map as needed). Dropping a task drops a fetch or a scroll command.
  3. Map the view: VirtualGrid::view renders 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 rowsgrid::VirtualGrid::set_follow (auto-freezes when the user scrolls up)
sort / filter programmaticallygrid::VirtualGrid::set_spec + grid::VirtualGrid::spec
wire keyboard navigationgrid::VirtualGrid::focus_next / focus_prev / page_up / page_down — bind them to any keys
read / drive selectiongrid::VirtualGrid::selection / selection_mut
persist and restore the scroll positiongrid::VirtualGrid::scroll_anchor / scroll_to_anchor (row-space: stable under churn)
restyle rows/cells/bandsthe set_*_style hooks on grid::VirtualGrid, wrapping the public default_* fns
apply a whole looktheme::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 boundaryimplement oriel_core::data::DataSource
fit columns to contentgrip 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:

Statereadrestore
column widthscolumn_layout (placements())column_layout_mut + set_width (doc example there)
sort + filterspec (serde-ready with oriel-core’s serde feature)set_spec
scroll positionscroll_anchor (row-space — stable under churn)scroll_to_anchor
selectionselection (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 moves

For 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 Column render 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 to Widget only where composition can’t, here because mouse_area cannot 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 animateRows pair):
theme
Ready-made grid themes — faithful ports of the looks users already know.