1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! `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.