Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/docview-design.md

facett-docview — design

Class: design · Function axis: f0 (the render pipeline) + f1 page/font parse · f2 the lazy PageSource seam · f3 texture cache + viewport scroll · f4 the Elm/Facet contract. Strict: this file states only the shape we run. Every why, every alternative, lives in docview-reasoning.md. Workflow lives in docview-guide.md.

f0 — the render pipeline (the whole)

DocView (facett-docview/src/lib.rs:372) shows a manual as a stack of vector SVG pages: a TOC sidebar, continuous vertical scroll, stepped zoom + fit-width, and a page-jump box — pure Rust, no pdfium, no native libraries. The pipeline is one dependency, resvg, which re-exports usvg and tiny_skia (lib.rs:51-52):

  1. Parse once per page — DocPage::from_svg_bytes (lib.rs:125) → usvg::Tree::from_data(bytes, &opts); the intrinsic page size in pt is read from tree.size() and kept on the DocPage (width_pt/height_pt, lib.rs:110-115).
  2. Rasterize on demand, at a zoom bucket — DocPage::rasterize (lib.rs:149) allocates a tiny_skia::Pixmap sized to page_pt × scale, applies a uniform Transform::from_scale, calls resvg::render, then demultiplies the premultiplied RGBA into an egui::ColorImage (lib.rs:162-169).
  3. Cache by (page_index, zoom_bucket) — DocView::cache (lib.rs:382), a RefCell<HashMap<(usize, u32), egui::TextureHandle>>; DocView::zoom_bucket (lib.rs:579) quantizes zoom × pixels_per_point to ~2% steps so a page re-rasterizes only when its bucket actually changes.
  4. Viewport-bounded — DocView::view (lib.rs:638) computes the visible band from ScrollArea::both().show_viewport (lib.rs:760-855); only pages within VIEWPORT_MARGIN_FACTOR (lib.rs:67) of the band rasterize, and DocView::evict (lib.rs:593) drops cache entries and (in lazy mode) parsed pages outside that band.
  5. Clamped — zoom ∈ [MIN_ZOOM, MAX_ZOOM] = [0.5, 4.0] (lib.rs:55-57), a page's pixmap never exceeds MAX_TEX_DIM = 4096px in either dimension (lib.rs:62, enforced in DocPage::rasterize at lib.rs:154-157).

Reuse map (what docview uses): resvg/usvg/tiny_skia (the render stack, re-exported at lib.rs:51-52), rayon (parallel page parse + rasterize), and facett-core's Elm trait, impl_facet_via_elm! bridge, harness module, scroll_engine::{SmoothScroll, smooth_scroll_area}, and clip::{ClipKind, ClipPayload, CopySource}. It is used by facett-demo's docview tab (facett-demo/src/lib.rs:633) and by any host embedding a Typst- or skade-exported manual.

f1 — page parse + the shared fontdb

DocPage::from_svg_bytes (lib.rs:125) parses with a usvg::Options whose fontdb field is shared_fontdb() (lib.rs:87) and whose font_family is EMBEDDED_SANS_FAMILY (lib.rs:128, :82). shared_fontdb builds the database once, behind a static OnceLock<Arc<...>> (lib.rs:88), and every subsequent call clones the Arc — the expensive db.load_system_fonts() (lib.rs:92) runs exactly once per process. After system fonts, it loads an embedded font, EMBEDDED_SANS — include_bytes!("../assets/DejaVuSans.ttf") (lib.rs:79) — and sets it as the sans/serif/monospace/cursive/fantasy default family (lib.rs:97-101), so any <text> with no usable font-family still shapes.

This lets DocPage::from_svg_bytes parse two different kinds of SVG through the same path:

SVG kind text representation fontdb involvement
Typst-exported manual page every glyph outlined as a vector path none — outlines render regardless of fontdb
hand-authored diagram (e.g. skade) real <text> elements with font-family resolved against shared_fontdb, falling back to the embedded DejaVu Sans

f2 — the lazy PageSource seam

Pages (lib.rs:199) is the page backing, one of two variants:

PageSource (lib.rs:182) is the only seam for lazy loading: page_count, page_svg(i) -> Option<Vec<u8>>, and page_size_hint() (default (595.0, 842.0), A4 portrait, lib.rs:191). It is Send + Sync so Pages::ensure_parsed (lib.rs:247) fetches + parses the needed indices in parallel via rayon's par_iter (lib.rs:255-261), one page per core. A parsed page's real size refines size_hint (lib.rs:263-266) so pre-fetch layout converges to the true page box. Pages::evict_parsed (lib.rs:289) drops cached DocPages outside the current viewport band; Pages::rasterize_many (lib.rs:274) rasterizes a batch of pages in parallel, again via rayon.

f3 — texture cache + viewport scroll

DocView::view (lib.rs:638) drives the scroll/cache machinery each frame:

  1. Geometry pass — for every page, its logical box (page_logical_size, lib.rs:585, = page_pt × zoom) and stacked top offset are precomputed (lib.rs:735-743); the visible band is [viewport.min.y − margin, viewport.max.y + margin] where margin = viewport.height × VIEWPORT_MARGIN_FACTOR (lib.rs:767).
  2. Velocity-biased prefetch — the band leads in the scroll direction by scroll_delta × PREFETCH_LEAD (lib.rs:72, :771-776), clamped to two screens, so an accelerating fling has its upcoming pages parsed + rasterized before they enter view.
  3. Rasterize pass — pages in the band not already in DocView::cache at the current zoom_bucket are parsed (if lazy) then rasterized in parallel (lib.rs:813-832), and each result is uploaded via ui.ctx().load_texture with egui::TextureOptions::LINEAR (lib.rs:825-830).
  4. Draw pass — each visible page is painted from the cache with ui.painter().image (lib.rs:834-848).
  5. Smooth scroll — DocView::page_scroll_x / page_scroll_y (lib.rs:394-395, both Cell<SmoothScroll>) drive the page stack through SmoothScroll::follow (facett-core/src/scroll_engine.rs:46-area); a pending Msg::JumpTo snaps the eased offset to the target page's top (lib.rs:754-758) so a bookmark lands immediately, while ordinary wheel scrolling is eased. The TOC sidebar scrolls through smooth_scroll_area (facett-core/src/scroll_engine.rs:163), called from draw_sidebar (lib.rs:610-625).
  6. Solid, non-floating scrollbar — ui.spacing_mut().scroll = egui::style::ScrollStyle::solid() (lib.rs:720) reserves the scrollbar its own lane; fit_width's target column width is the panel width minus that reserved lane (lib.rs:721-727), so a fitted page's right edge never sits under the bar.
  7. Eviction — DocView::evict (lib.rs:593) runs every frame with the current visible set and zoom bucket, dropping stale cache entries and (lazy mode) far-offscreen parsed pages.

f4 — the Elm / Facet contract

DocView implements facett_core::Elm (lib.rs:927) with Model = DocState, Msg = Msg, Effect = Effect:

state_json() shape (lib.rs:961-972):

{
  "page_count": 4, "current_page": 0, "zoom": 1.0, "sidebar": true,
  "toc": [{"title": "Welcome", "page": 0}, ...],
  "visible_pages": [0, 1],
  "cache_size": 2,
  "page_sizes_pt": [[420.0, 594.0], ...]
}