facett-docview — design
Class:
design· Function axis:f0(the render pipeline) +f1page/font parse ·f2the lazyPageSourceseam ·f3texture cache + viewport scroll ·f4the Elm/Facet contract. Strict: this file states only the shape we run. Every why, every alternative, lives indocview-reasoning.md. Workflow lives indocview-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):
- 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 fromtree.size()and kept on theDocPage(width_pt/height_pt,lib.rs:110-115). - Rasterize on demand, at a zoom bucket —
DocPage::rasterize(lib.rs:149) allocates atiny_skia::Pixmapsized topage_pt × scale, applies a uniformTransform::from_scale, callsresvg::render, then demultiplies the premultiplied RGBA into anegui::ColorImage(lib.rs:162-169). - Cache by
(page_index, zoom_bucket)—DocView::cache(lib.rs:382), aRefCell<HashMap<(usize, u32), egui::TextureHandle>>;DocView::zoom_bucket(lib.rs:579) quantizeszoom × pixels_per_pointto ~2% steps so a page re-rasterizes only when its bucket actually changes. - Viewport-bounded —
DocView::view(lib.rs:638) computes the visible band fromScrollArea::both().show_viewport(lib.rs:760-855); only pages withinVIEWPORT_MARGIN_FACTOR(lib.rs:67) of the band rasterize, andDocView::evict(lib.rs:593) drops cache entries and (in lazy mode) parsed pages outside that band. - Clamped — zoom ∈
[MIN_ZOOM, MAX_ZOOM]=[0.5, 4.0](lib.rs:55-57), a page's pixmap never exceedsMAX_TEX_DIM= 4096px in either dimension (lib.rs:62, enforced inDocPage::rasterizeatlib.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:
Pages::Eager(Vec<DocPage>)— every page parsed up front, built byDocView::new(lib.rs:403) /DocView::from_svgs(lib.rs:448).Pages::Lazy { source, count, cache, size_hint }— pages fetched + parsed only as they near the viewport, built byDocView::from_source(lib.rs:413).
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:
- 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]wheremargin = viewport.height × VIEWPORT_MARGIN_FACTOR(lib.rs:767). - 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. - Rasterize pass — pages in the band not already in
DocView::cacheat the currentzoom_bucketare parsed (if lazy) then rasterized in parallel (lib.rs:813-832), and each result is uploaded viaui.ctx().load_texturewithegui::TextureOptions::LINEAR(lib.rs:825-830). - Draw pass — each visible page is painted from the cache with
ui.painter().image(lib.rs:834-848). - Smooth scroll —
DocView::page_scroll_x/page_scroll_y(lib.rs:394-395, bothCell<SmoothScroll>) drive the page stack throughSmoothScroll::follow(facett-core/src/scroll_engine.rs:46-area); a pendingMsg::JumpTosnaps 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 throughsmooth_scroll_area(facett-core/src/scroll_engine.rs:163), called fromdraw_sidebar(lib.rs:610-625). - 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. - 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:
DocState(lib.rs:343,Serialize + Deserialize + PartialEq) —zoom,show_sidebar,current_page,visible_pages,toc,scroll_to. This is the complete observable Model (FC-1/FC-3); the parsed/lazyPagesengine and the GPU texture cache are not in it — they live inRefCell/Cellfields onDocView(lib.rs:376-396).Msg(lib.rs:307) —ToggleSidebar,ZoomIn,ZoomOut,SetZoom(f32),FitWidth(f32),JumpTo(usize),SyncViewport { current_page, visible, consumed_scroll_to }. Every toolbar button, TOC click, jump-box Enter, and the per-frame scroll-derived sync route through this one enum (FC-2).Effect(lib.rs:333) — an uninhabited enum:DocView::update(lib.rs:543) never asks the host to do I/O (FC-8).update(lib.rs:543) is the sole&mut selfmutation ofDocState;view(lib.rs:638) takes&selfand returns the frame'sMsgs (FC-9) — it advances the render-engineRefCell/Cellmachinery (cache, lazy parse/evict, smooth-scroll) through interior mutability, but never writesDocStatedirectly.- The bridge —
facett_core::impl_facet_via_elm!(DocView, custom_state_json, { … })(lib.rs:956) writesimpl Facet for DocView, using form 3 to supply a richerstate_jsonthat folds in non-Model engine signals (page_count,cache_size,page_sizes_pt) alongside the Model fields (lib.rs:961-972). - Caps —
caps()returnsFacetCaps::NONE.scalable().themeable().resizable().copyable()(lib.rs:977);scale()/set_scale()(lib.rs:982-987) make the deck's uniform zoom control driveDocState::zoomdirectly, sofit_width/zoom_inand the deck's Ctrl-+/-/0 operate on the same value. - Typed copy —
DocViewimplementsCopySource(facett-core/src/clip.rs:131):copy_kinds()returns&[ClipKind::Text](lib.rs:908), andcopy_payload()(lib.rs:911-923) serializes the TOC as"title\tpage"lines (1-based page numbers) — the page rasters carry no extractable text, but the outline does.
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], ...]
}