Gunnar

nordisk/facett

.nornir/docview-guide.md

# facett-docview — guide

> **Class:** `guide` · audience: a techie embedding or driving a `DocView`.
> The *what* is in [`docview-design.md`](docview-design.md); the *why* in
> [`docview-reasoning.md`](docview-reasoning.md). This file is the workflow.

## f0 — zero to a driven viewer (the whole workflow)

Three moves get a manual on screen and under headless test:

1. **Build it** — `DocView::from_svgs("Manual", svgs, toc)` for a bundled/small manual, or
   `DocView::from_source("Manual", Box::new(my_source), toc)` for a large one you don't want to
   parse all at once.
2. **Drop it in a deck** — `DocView` implements `facett_core::Facet`; box it like any other
   facet (`facett-demo/src/lib.rs:633,786` does exactly this).
3. **Drive it headlessly** — every gesture is a `Msg`; `facett_core::harness` applies a `Msg`
   script and returns a snapshot or a `RenderReport`, so a test never opens a window.

## f1 — constructing a `DocView`

```rust
use facett_docview::{DocView, DocPage, PageSource};

// Eager: parse every page up front (rayon, 1 page/core) — good for small/medium manuals.
let view = DocView::from_svgs(
    "Manual",
    [page1_svg_bytes, page2_svg_bytes /* , .. */],
    vec![("Introduction".into(), 0), ("Chapter 2".into(), 1)],   // (toc title, 0-based page index)
);

// Or pre-parse pages yourself:
let pages = vec![DocPage::from_svg_bytes(bytes)?, /* .. */];
let view = DocView::new("Manual", pages, toc);
```

Failed parses are skipped, not fatal: `from_svgs`/`new` drop unparseable pages, so
`view.page_count()` can be less than the input length.

## f2 — lazy loading a huge manual via `PageSource`

Implement `PageSource` over wherever your pages live (an archive, a compressed blob, a
network store) and hand it to `from_source`. The viewer never sees your storage — it only asks
for page `i`'s bytes when `i` scrolls near the viewport:

```rust
struct ArchiveSource { /* .. */ }

impl PageSource for ArchiveSource {
    fn page_count(&self) -> usize { /* total pages */ }
    fn page_svg(&self, i: usize) -> Option<Vec<u8>> { /* fetch + return page i's SVG bytes */ }
    fn page_size_hint(&self) -> (f32, f32) { (595.0, 842.0) } // A4 default; refine if you know better
}

let view = DocView::from_source("Manual", Box::new(ArchiveSource::open(path)?), toc);
```

`page_count()` returns instantly (no parsing); the viewer fetches + parses only the pages your
current scroll position and its prefetch lead touch, and drops them again once they scroll far
enough away.

## f3 — controls

All controls route through the same `Msg` enum a headless driver uses, so live and scripted
behavior are identical:

| method | effect |
|---|---|
| `zoom_in()` / `zoom_out()` | stepped zoom (× / ÷ `ZOOM_STEP`), clamped to `[MIN_ZOOM, MAX_ZOOM]` |
| `set_zoom(z)` | zoom to `z`, clamped |
| `fit_width(panel_px)` | `zoom = panel_px / first_page_width_pt`, clamped |
| `jump_to(page)` | scroll to 0-based `page` (clamped to the last page) next render |
| `toggle_sidebar()` | show/hide the TOC panel |

Read-backs: `page_count()`, `current_page()`, `zoom()`, `toc()`, `visible_pages()`,
`cache_len()`, `state_json()`.

## f4 — the headless harness seam

```rust
use facett_core::harness;

let mut v = DocView::from_svgs("Manual", svgs, toc);

// Drive a Msg script and read the resulting Model — no window, no GPU:
let model = harness::snapshot(&mut v, [Msg::ZoomIn, Msg::JumpTo(1), Msg::ToggleSidebar]);
assert_eq!(model.current_page, 1);

// Confirm it actually paints (rasterizes + tessellates) offscreen:
let report = harness::headless_render(&mut v);
assert!(report.drew());
assert_eq!(report.state["page_count"], model_page_count);
```

`harness::drive` returns the (always-empty, for docview) `Vec<Effect>`; `harness::snapshot_json`
gives a golden-file-friendly `serde_json::Value` of the `DocState`.

## f5 — typed copy

`DocView` implements `facett_core::clip::CopySource`, so a host's generic "copy" action gets the
table of contents as text (`"title\tpage"` per line, one page per row):

```rust
use facett_core::clip::CopySource;
if let Some(payload) = view.copy_payload() {
    let text = payload.as_text(); // "Introduction\t1\nChapter 2\t2"
}
```

## f6 — running the demo

`facett-demo` wires a **docview** tab over a bundled sample manual
(`facett-demo/src/data.rs:427` `sample_manual()` — synthetic Typst-style SVG pages + a TOC).
Run it:

```
CARGO_TARGET_DIR=/home/rickard/.cargo-target-facett cargo run -p facett-demo
```

and pick the **docview** tab. The mega-matrix, self-check, and robot-UI live-drive rows in
facett-demo (`facett-demo/src/lib.rs:1573-1577`) all exercise it, including caps and jump-to-page.