oriel-iced 0.1.0

iced integrator for the oriel headless data grid: satisfies the data and render seams for iced 0.14.
Documentation
//! `oriel-iced` — a virtualized, batteries-included data grid for
//! [iced](https://iced.rs) 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.
//!
//! ```no_run
//! 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`](grid::VirtualGrid::update).
//! 2. **Run the returned tasks**: `update` — and every imperative method
//!    ([`transact`](grid::VirtualGrid::transact),
//!    [`set_spec`](grid::VirtualGrid::set_spec),
//!    [`set_follow`](grid::VirtualGrid::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`](grid::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 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`](grid::VirtualGrid::focus_prev) / [`page_up`](grid::VirtualGrid::page_up) / [`page_down`](grid::VirtualGrid::page_down) — bind them to any keys |
//! | read / drive selection | [`grid::VirtualGrid::selection`] / [`selection_mut`](grid::VirtualGrid::selection_mut) |
//! | persist and restore the scroll position | [`grid::VirtualGrid::scroll_anchor`] / [`scroll_to_anchor`](grid::VirtualGrid::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`](theme::GridTheme::apply)) |
//! | render a custom cell (chips, pills, sparklines) | implement [`oriel_core::columns::Column`] — see [`column`](mod@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`](grid::VirtualGrid::focus_next),
//! [`focus_prev`](grid::VirtualGrid::focus_prev),
//! [`page_up`](grid::VirtualGrid::page_up) /
//! [`page_down`](grid::VirtualGrid::page_down), and
//! [`set_follow`](grid::VirtualGrid::set_follow) /
//! [`scroll_to_row`](grid::VirtualGrid::scroll_to_row) for End/Home:
//!
//! ```no_run
//! # use iced::Subscription;
//! # use oriel_core::data::VecStore;
//! # use oriel_iced::grid::{self, VirtualGrid};
//! # #[derive(Clone, Debug)] struct Row { id: u64 }
//! # type Store = VecStore<Row, u64, fn(&Row) -> u64>;
//! # struct App { grid: VirtualGrid<Store> }
//! # #[derive(Debug, Clone)] enum Msg { Grid(grid::Message<Row>), Key(iced::keyboard::Event) }
//! 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`](grid::VirtualGrid::column_layout) (`placements()`) | [`column_layout_mut`](grid::VirtualGrid::column_layout_mut) + `set_width` (doc example there) |
//! | sort + filter | [`spec`](grid::VirtualGrid::spec) (serde-ready with oriel-core's `serde` feature) | [`set_spec`](grid::VirtualGrid::set_spec) |
//! | scroll position | [`scroll_anchor`](grid::VirtualGrid::scroll_anchor) (row-space — stable under churn) | [`scroll_to_anchor`](grid::VirtualGrid::scroll_to_anchor) |
//! | selection | [`selection`](grid::VirtualGrid::selection) (iterate the `RowId`s) | [`selection_mut`](grid::VirtualGrid::selection_mut) |
//!
//! # Testing your integration
//!
//! The grid is a plain value — no window or renderer needed for state-level
//! tests: drive [`update`](grid::VirtualGrid::update) with synthesized
//! messages and assert on the exposed state.
//!
//! ```
//! # use oriel_core::{columns::ColumnId, data::VecStore};
//! # use oriel_iced::grid::{BoxedColumn, Message, VirtualGrid};
//! # use oriel_iced::column::TextColumn;
//! # #[derive(Clone, Debug)] struct Row { id: u64 }
//! # fn key(r: &Row) -> u64 { r.id }
//! # let store = VecStore::new((0..10).map(|id| Row { id }).collect(), key as fn(&Row) -> u64);
//! # let columns: Vec<BoxedColumn<Row>> =
//! #     vec![Box::new(TextColumn::new(ColumnId(1), "#", 80.0, |r: &Row| r.id.to_string()))];
//! # let (mut grid, _init) = VirtualGrid::new(store, columns);
//! # let _ = grid.transact(|_| {}); // fill the fetch window synchronously
//! 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`](grid::VirtualGrid), the widget itself: its
//!   messages, knobs, hooks, and public style defaults.
//! - [`column`](mod@column) — provided column renderers
//!   ([`TextColumn`](column::TextColumn), [`BarColumn`](column::BarColumn))
//!   and the header font.
//! - [`theme`] — [`GridTheme`](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.

// Machinery, not habit: the public surface is documented
// to docs.rs standard — keep it that way by construction.
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]

pub mod arrow;
pub mod column;
pub mod grid;
pub mod measure;
pub mod resize;
pub mod scrollbar;
pub mod slide;
pub mod theme;