zttext API

← zttext · version 0.1.6

84 public items, in source order. Generated from the registry index, which re-derives them from the package source at publish time — so a signature here is the one that version actually ships.

pub fn zttext_version() -> text

The version of this source — the version in its loft.toml, bumped with it.

pub struct Piece { start: integer, count: integer, style: integer }

A piece p = ⟨o, ℓ, s⟩ references the buffer span B[start, start+count) with style-id style. Pieces are immutable value objects: an edit REBUILDS the piece sequence, it never mutates a piece or swaps vector slots in place.

pub struct Doc { buf: vector<character>, pieces: vector<Piece> }

The content model. buf is the append-only buffer B ∈ Σ* (a vector<character>, so offsets are characters); pieces is the piece sequence D = ⟨p₁…pₙ⟩. The materialised text ⌊D⌋ is the concatenation of each piece's buffer span. (Bundling B with D keeps the engine's surface small; the buffer is treated as append-only within an edit.)

pub struct Run { str: text, style: integer }

A styled run: a maximal same-style character span. slice_runs returns these; they are what an inverse Ins/Sty re-creates so undo restores styles, not just text.

pub fn empty_doc() -> Doc

A document with no text. The starting point every edit sequence builds from, and what an undo to the beginning arrives at.

pub fn doc_from_text(s: text, style: integer) -> Doc

build a one-piece doc holding s at style-id style.

pub fn materialise(d: Doc) -> text

⌊D⌋ — the logical text of the doc (concatenate every piece's buffer span in order).

pub fn doc_len(d: Doc) -> integer

|⌊D⌋| — the character length of the logical text.

pub fn locate(d: Doc, a: integer) -> (integer, integer)

locate(D, a) → (piece index, in-piece offset). Anchor a ∈ [0,|⌊D⌋|] sits BETWEEN characters; piece i covers logical [Cᵢ, Cᵢ+countᵢ). A boundary a = Cᵢ₊₁ lands in piece i+1 at offset 0; a = |⌊D⌋| (the end) returns the sentinel (n_pieces, 0).

pub fn locate_inverse(d: Doc, pi: integer, ro: integer) -> integer

inverse of locate: (piece index, in-piece offset) → the logical anchor a.

pub fn insert_text(d: Doc, a: integer, s: text, style: integer) -> Doc

insert text s (style-id style) at anchor a. Appends s's characters to a fresh buffer (append-only), then rebuilds the piece sequence splitting the piece at a.

pub fn delete_range(d: Doc, a: integer, b: integer) -> Doc

delete the range [a,b). Keeps each piece's portion outside [a,b); the buffer is UNCHANGED (deleted characters stay in B so undo can restore them).

pub fn set_style(d: Doc, a: integer, b: integer, style: integer) -> Doc

set style-id style on the range [a,b): split each overlapping piece three ways ([ps,a) old · [a,b) new · [b,pe) old) and restyle the middle.

pub fn slice_runs(d: Doc, a: integer, b: integer) -> vector<Run>

the styled runs of [a,b): one Run per overlapping piece-fragment, carrying its text and style. Concatenating the run texts is exactly ⌊D⌋[a,b); the styles are χ_D over [a,b).

pub fn materialise_range(d: Doc, a: integer, b: integer) -> text

materialise just [a,b) as text.

pub fn char_styles(d: Doc) -> vector<integer>

per-character style vector over the whole doc (χ_D as a concrete list) — the reference against which piece-blindness (I2) and undo are checked.

pub enum Op { Ins { pub at: integer, pub str: text, pub style: integer }, Del { pub from: integer, pub to: integer }, Sty { pub from: integer, pub to: integer, pub style: integer } }

The primitive edit operations (notation §8): ins(a,t,σ) · del(a,b) · sty(a,b,σ).

pub struct Txn { ops: vector<Op> }

A transaction τ — an atomic, ordered composition of primitive ops; the unit of undo.

pub fn txn_of(ops: vector<Op>) -> Txn

Bundle ops into ONE transaction — the unit of undo. Grouping them here is what makes a compound edit undo in a single step rather than in pieces.

pub fn apply_op(d: Doc, op: Op) -> Doc

Apply one primitive op and answer the new document. The dispatch every higher-level edit funnels through, so Ins / Del / Sty have exactly one meaning each. An unrecognised op answers the document unchanged.

pub fn apply(d: Doc, t: Txn) -> Doc

apply a whole transaction, left to right. Only the pre- and post-states are observable.

pub fn inv_op_group(pre: Doc, op: Op) -> vector<Op>

the inverse of a single op against its PRE-state pre, as an ordered op list (a delete or restyle over a multi-style range inverts to one op per style-run, so undo restores styles exactly, not just text).

pub fn invert(d: Doc, t: Txn) -> Txn

invert(D, τ) → τ⁻¹ such that apply(apply(D,τ), τ⁻¹) ≈ D (same text + same per-char styles). Each op's inverse is computed against its pre-state; the groups are emitted in reverse op order (prepend each new group), since τ⁻¹ undoes the last op first.

pub struct Style { size: float, weight: integer, italic: boolean, dir: integer }

A concrete style σ ∈ 𝕊. For the fake monospace measurer only size (per-character advance) matters; weight/italic/dir (0 = LTR, 1 = RTL) round out the shape the real backends will fill in. Layout reads styles ONLY through a StyleResolver, so a surface restyles by swapping the resolver.

pub fn default_style() -> Style

Size 1, regular, upright, left-to-right — the style a document starts in and the one to adjust from rather than building a Style field by field.

pub fn mono_measure(s: text, st: Style) -> float

The fake monospace Measurer μ: advance width = character-count × style.size. Pure and deterministic, so every layout milestone is headless and parity-testable. The Measurer PORT type is fn(text, Style) -> float; this is one implementation (a named ref you pass where a Measurer is expected).

pub fn default_resolver(_id: integer) -> Style

A trivial StyleResolver ρ: every id resolves to the default style. The port type is fn(integer) -> Style; a real surface passes its own (document stylesheet · IDE syntax theme · cell format).

pub fn run_height(st: Style) -> float

The fake box height of a run in style st. Real backends give ascent+descent; the fake uses square cells (height = size) so line stacking is deterministic and hand-checkable.

pub struct Rect { x: float, y: float, w: float, h: float }

The render IR (notation §4). A Rect is a plane rectangle; a Box β = ⟨rect, κ, style⟩ carries the LOGICAL content-ref κ = [ks, ke) (character span in ⌊D⌋) and a single style. The rect's position is the VISUAL order; κ is the LOGICAL order — both explicit in every box, which is the whole BiDi story (a selection is κ-intersection, geometry-free).

pub struct Box { rect: Rect, ks: integer, ke: integer, style: integer, hyphen: boolean = false }

hyphen marks a synthetic line-end hyphen glyph (from a discretionary break, S4.7): it renders "-" rather than its κ text, and κ points at the soft-hyphen it stands in for.

pub struct BoxSet { boxes: vector<Box> }

The boxes of a laid-out text: one Box per glyph run, in layout order. What flow_layout returns.

pub struct LaidLine { cs: integer, ce: integer, y: float, h: float, para_end: boolean = false, right: float = 0.0 }

A laid-out visual line: the logical char span [cs,ce) its boxes cover, its top y and its height. para_end is true when this is a paragraph's LAST line (ended at a hard break or the doc end) — it is left ragged (justify skips it). right is the line's available right edge (the column edge, or an image's edge for a line beside one) — the target justify fills to. Carried by a Layout so incremental reflow can find the lines an edit touches (M3).

pub struct PlacedImage { x: float, y: float, w: float, h: float, id: integer }

A placed image on the page: its rectangle + the image reference id (a file_record id at the app layer). It is BOTH a rendered element AND an exclusion region the text steers around.

pub struct Layout { boxes: vector<Box>, lines: vector<LaidLine>, images: vector<PlacedImage> }

A full layout result: the text boxes (the render IR), per-line metadata (the seam incremental reflow needs), and the placed images (rendered separately; text never overlaps them). flow_layout returns just the BoxSet; flow_layout_full returns this.

pub struct Token { word: boolean, ts: integer, te: integer, disc: boolean = false, brk: boolean = false }

A token in the stream: a word (non-space run), glue (a space run), a discretionary break (disc, a soft hyphen), or a paragraph break (brk, a newline run). word/disc/brk are the kind flags; all false → glue. Glue is the breakable inter-word space; disc is a zero-width in-WORD break (S4.7); brk is a HARD break that ends the line and starts a new paragraph (a paragraph's last line is ragged, and gets paragraph spacing after it).

pub fn soft_hyphen() -> text

The canonical discretionary-hyphen character: Unicode SOFT HYPHEN U+00AD (UTF-8 0xC2 0xAD). It is the PORTABLE storage form — invisible in any conforming Markdown/HTML pipeline, and rendered as a hyphen only when a line breaks at it. The [-] authoring notation maps to it.

pub fn encode_shy(s: text) -> text

encode the [-] authoring notation → the canonical soft hyphen (import / editor-input path).

pub fn decode_shy(s: text) -> text

decode the canonical soft hyphen → the [-] notation (editor source-view / round-trip).

pub fn seg(d: Doc) -> vector<Token>

seg(⌊D⌋) → the token stream T of words ω, glue δ, and discretionary breaks (soft hyphens). A soft hyphen becomes its own zero-width Disc token; words are maximal non-space, non-soft-hyphen runs, so every inter-token boundary is a legal break (space or hyphen).

pub fn token_width(d: Doc, tok: Token, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> float

ν(t) — the measured advance of a token: the sum over its style-runs of μ(run, ρ(style)). (A token may cross a style boundary; each run measures under its own resolved style.)

pub struct Line { first: integer, last_excl: integer }

A laid-out line: the half-open token range [first, last_excl). Trailing glue at a break is NOT included (notation §6: "trailing glue on a line is dropped").

pub fn lines_greedy(d: Doc, toks: vector<Token>, w_col: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> vector<Line>

greedy line breaking (notation §6, brk): pack words while they fit in width W; interior glue counts toward the width, trailing glue is deferred and dropped at a break. The first word on a line always fits (an over-wide word overflows onto its own line, no infinite loop).

pub fn next_line(d: Doc, toks: vector<Token>, from: integer, w_col: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> (integer, integer, integer, boolean)

Break ONE line greedily starting at token from → (last_excl, next_tok, hyphen_disc, para_break): the line covers tokens [from, last_excl); the next line starts at next_tok; hyphen_disc is the token index of a soft hyphen the line broke a word at (a trailing hyphen glyph), else -1; para_break is true when the line ended at a HARD paragraph break (which is consumed — next_tok is past it). The per-line step both the full layout and the patch reuse.

pub struct PlacedLine { boxes: vector<Box>, line: LaidLine }

A placed line: its boxes plus its LaidLine metadata.

pub fn place_line(d: Doc, toks: vector<Token>, first: integer, last_excl: integer, hyphen_disc: integer, para_end: boolean, y: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> PlacedLine

Place tokens [first, last_excl) at top y — one box per style-run, x-advance left to right; the shared placement primitive so the full layout and a patched tail are byte-identical. If hyphen_disc >= 0, append a trailing hyphen glyph box for that soft hyphen (S4.7).

pub fn layout_tokens_from(d: Doc, toks: vector<Token>, y0: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float, para_gap: float = 0.0) -> Layout

Lay out a TOKEN LIST (absolute char spans) from top y0 — the whole-doc layout, built as a sequence of next_line + place_line steps (the exact steps an incremental patch reuses).

pub fn flow_layout_full(d: Doc, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float, para_gap: float = 0.0) -> Layout

L for the flow surface: content + resolver + measurer + column width → a full Layout. para_gap is the extra vertical space after each paragraph (0 = paragraphs still break onto new lines, just with no gap).

pub fn flow_layout(d: Doc, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float) -> BoxSet

The BoxSet-only view of a flow layout (the M1/M2 surface).

pub fn line_assignment(d: Doc, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float) -> vector<LaidLine>

D1 (EDITOR_DESIGN step D) — the LINE ASSIGNMENT: the per-line logical char spans [cs,ce) a document breaks into (each with its y/height, its justify edge right, and whether it is a paragraph's ragged last line). It is a PURE function of (content, styles, metrics, column), and it is the layer the determinism budget lives at (§8.22): common word widths ⇒ identical breaks ⇒ identical assignment on every platform. This is what a screen RenderSink paints one element PER (step D2), and what the loft-native PDF emitter (step E) paginates from — both downstream of, and agreeing because of, this one assignment. Materialise a line's text with materialise_range(d, cs, ce).

pub fn render_dump(bs: BoxSet, d: Doc) -> text

A headless RenderSink R: a deterministic textual dump of the BoxSet, one line per box — [x,y,w,h] ks..ke s=style 'text'. Golden-string comparable; the eyeball/regression view that stands in for a real DOM/Canvas/PDF sink (which is a Tier-2 backend, swapped at the browser edge). Coordinates are printed to one decimal so the string is backend-stable.

pub fn render_commands(l: Layout, d: Doc) -> text

The DOM/Canvas RenderSink R : 𝔅 → paint commands — the reusable engine-level sink PROTOCOL. A thin per-target host (browser host.js / native / PDF) paints these; loft owns every coordinate (§8.22), so the geometry is identical on every reader and the host only positions boxes it is handed. One LINE|y|h|right|paraEnd per laid line (the justified line assignment, D2) then one BOX|x|y|w|h|style|text per glyph-box (CSS px, 2 decimals so the string is backend-stable). This is CONTENT-ONLY: caret/selection overlays are a consumer concern layered on top (cf. ztedit's ed_render, which adds SCALE/DOC/CARET/SEL around this same LINE/BOX core), so any read surface — viewer, editor, print preview — reuses it unchanged. Takes a full Layout (lines + boxes).

pub fn boxset_height(bs: BoxSet) -> float

The total laid-out height (sum of line heights) — the column's content height.

pub fn box_at(bs: BoxSet, x: float, y: float) -> integer

box_at(𝔅, x, y) → the index of the box CONTAINING (x,y) (half-open rect), or -1 if the point is in no box (a margin / gap — anchor_at then falls back to the nearest box).

pub fn anchor_at(bs: BoxSet, d: Doc, x: float, y: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> integer

H(q, 𝔅) — the logical anchor for a point: pick the box (containing, else nearest), then refine x WITHIN the box by finding the character split whose prefix width is closest to the local x (via μ, so it is correct for proportional fonts, not just monospace). (RTL refinement, where the prefix mapping reverses, is completed with BiDi placement in S4.5; this LTR refinement is exact for the flow surface.)

pub fn sel(bs: BoxSet, a: integer, b: integer) -> vector<integer>

Sel(a,b) = { β : κ(β) ∩ [a,b) ≠ ∅ } — the boxes a logical selection [a,b) covers, returned as box indices. Defined PURELY by κ-intersection: it never mentions geometry, so a range crossing a direction boundary yields the correct (possibly visually-split) box set with no special-case code. THIS is the whole BiDi selection story.

pub fn sel_rects(bs: BoxSet, d: Doc, a: integer, b: integer, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> vector<Rect>

Sel(a,b) as precise highlight RECTANGLES — character-granular. Each box the range covers is CLIPPED to its selected sub-span [max(a,ks), min(b,ke)), the endpoints mapped to x within the box via μ. So a partially-selected word highlights only the selected characters, not the whole word (sel returns whole boxes; this returns the sub-rects to paint). A fully-selected box yields its whole rect; the boundary boxes are the fine ones.

pub fn caret_at(bs: BoxSet, d: Doc, a: integer, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> Rect

caret_at(a) — the caret rectangle (zero width) for logical anchor a: the x is refined within the box holding a (a==ke → the box's right edge; a==ks → its left), y/h from that box. The dual of anchor_at (point → anchor); this is anchor → point, for drawing the cursor.

pub fn word_at(d: Doc, a: integer) -> (integer, integer)

word(a) — the [ts,te) span of the word containing anchor a (what a double-click selects). A discretionary break does NOT end a word, so a hyphenated word selects whole: expand across adjacent word/disc tokens, stopping at glue. a in whitespace returns that glue run.

pub fn line_boxes(bs: BoxSet, a: integer) -> vector<integer>

line(a) — the box indices on the VISUAL line containing anchor a. A visual line is the maximal run of boxes sharing box@a's y that is CONTIGUOUS in reading order — not every box at that y. This matters for multi-column pages: column 1's line 3 and column 2's line 3 have the same absolute y, but they are laid out in reading order (all of column 1, then column 2), so a same-y run stays within one column. (A degenerate page with columns only one line tall would put two columns' first lines adjacent at the same y; real pages are taller, and layout_columns emits boxes column-major, so the run boundary is exact.)

pub fn doc_chars(d: Doc) -> vector<character>

the doc's logical text as a character vector (for the styled-string diff).

pub fn dirty(old_d: Doc, new_d: Doc) -> (integer, integer, integer)

dirty(D, D′) → (lo, hi_old, hi_new): the minimal changed span, computed by diffing the STYLED string (char AND style per position) — common prefix lo, common suffix trimmed. old[lo,hi_old) was replaced by new[lo,hi_new); the net char change is hi_new - hi_old. Diffing the observable styled string (not τ's op structure) gives the tightest range and makes patch independent of HOW the edit was expressed.

pub struct PatchResult { layout: Layout, recomputed: integer, reused: integer }

The result of an incremental relayout: the new Layout, plus how many boxes were RECOMPUTED (the tail) vs REUSED from the old layout (the prefix). The counts are the bounded-work evidence M5 asserts; the layout is what M3's differential checks against a full relayout.

pub fn patch(old_layout: Layout, old_doc: Doc, new_doc: Doc, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float) -> PatchResult

patch(L(D), D, D′) → a Layout EQUAL to L(D′) (the differential law I5), computed incrementally and with BOUNDED re-measurement (I7): reuse every line strictly before the edit verbatim, re-lay lines from there only until the break structure RECONVERGES with the old layout, then TRANSLATE the untouched tail (a cheap char+y shift, no re-measure).

Two correctness hinges: · start one line EARLIER than the line containing the edit — that earlier line's content is entirely before the edit, so the break INTO it is fixed; re-laying from there catches a "backward join" (a shrunk word rising onto the previous line); · reconvergence — once a re-laid line ends at a new-doc char nsc past the dirty region and some OLD line starts at nsc − delta, the token streams from there are identical (shifted by delta), so the greedy produces identical lines: reuse the old tail, shifted. First-line / before-all edits fall back to a full relayout (still trivially I5-correct).

pub fn render_dump_layout(l: Layout, d: Doc) -> text

The BoxSet dump of a Layout (for golden/differential comparison).

pub fn map_anchor(a: integer, op: Op) -> integer

map_anchor(a, op) — where a caret at logical anchor a lands after op. Insert strictly before a shifts it right (a stays attached to the same following text); insert at/after a leaves it (typed text lands after the caret). Delete before a shifts it left; delete after a leaves it; delete spanning a collapses it to the deletion start. Style ops don't move it.

pub fn map_anchor_txn(a: integer, t: Txn) -> integer

map a caret through a whole transaction (op by op, left to right).

pub struct Page { first_line: integer, last_excl: integer, first_char: integer, top_y: float }

A page: the line range [first_line, last_excl) it holds, plus the char and y at which it starts (its π entry — enough to lay the page out from scratch without the pages before it).

pub fn paginate(layout: Layout, page_h: float) -> vector<Page>

paginate a laid-out column: lines stack onto a page until the next line would exceed page_h, then a new page begins. The first line of a page always fits (even if taller than page_h). The page list IS the break-point index π (each Page.first_char = π(n)).

pub fn seek_page_char(pages: vector<Page>, n: integer) -> integer

seek(N) — the first CHARACTER of page N, read directly from π in O(1) (no laying out of pages 1..N−1). Pairs with layout_page to render page N from scratch (I6).

pub fn layout_page(d: Doc, start_char: integer, y0: float, page_h: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float) -> Layout

Lay out ONE page directly from a page-start (its first_char + top_y) — the same stop rule paginate uses, so it reproduces exactly that page's lines. This is "seek to page N without laying out 1..N−1": the work is one page of lines, independent of N (I6).

pub fn apply_bidi(layout: Layout, resolve: fn(integer) -> Style) -> Layout

Apply the UAX-9 L2 reorder (LTR/RTL subset) to a laid-out column: within each visual line, each maximal run of RTL boxes (resolved dir == 1) has its VISUAL order reversed — the boxes keep their logical κ, only their x changes. So x follows ψ while κ follows logical, and any κ-based operation (Sel, hit-test) is unaffected. Base direction is LTR (the flow surface).

pub fn justify(layout: Layout, d: Doc, w_col: float, justify_last: boolean = false) -> Layout

Justify each visual line by distributing its leftover space equally across its glue boxes (widen each, shift the rest right). The target right edge is the line's OWN available right (LaidLine.right) — the column edge, or an image's edge for a line beside one — so it works per column AND beside images (falls back to line_left + w_col for a hand-built layout). A paragraph's LAST line stays ragged UNLESS justify_last (the old-school "fill every line" look). Overfull or glue-less lines are left as-is. (Per-glue stretch/shrink BOUNDS are a refinement; this distributes equally.)

pub fn materialise_viewport(d: Doc, pages: vector<Page>, page_h: float, y_top: float, y_bottom: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, w_col: float) -> Layout

Materialise only the pages overlapping the vertical window [y_top, y_bottom) — the boxes a viewport (plus its page margin) actually shows. Each page is re-laid from its π entry, so the materialised box count tracks the VIEWPORT, not the document (I7): a 10× larger doc with the same viewport materialises the same boxes.

pub fn piece_count(d: Doc) -> integer

How many pieces the document is fragmented into. Edits split pieces, so this rises with editing and compact_step is what brings it back down — which makes it the measure to watch when deciding whether compaction is worth running.

pub fn compact_step(d: Doc, budget: integer) -> Doc

Coalesce up to budget adjacent piece pairs that reference a CONTIGUOUS buffer span with the SAME style (edits fragment the piece vector; those fragments often become mergeable again). Bounded per call (≤ budget merges) so it is interruptible — run it repeatedly in idle time. ⌊D⌋ is unchanged (a pure storage-layer coalesce), which is probe P3.

pub struct PageGeom { width: float, height: float, margin_left: float, margin_right: float, margin_top: float, margin_bottom: float, columns: integer, gutter: float, para_gap: float = 0.0 }

Page geometry: the physical page, its margins, and its column grid. Column width is derived so columns equal columns plus gutter gaps exactly fill the content box.

pub fn column_width(g: PageGeom) -> float

the width of one column: (content width − the inter-column gutters) / columns.

pub fn column_height(g: PageGeom) -> float

the usable height of a column (page height minus top/bottom margins).

pub fn layout_columns(d: Doc, g: PageGeom, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> Layout

Lay text out over a multi-column page grid in ABSOLUTE canvas coordinates (pages stacked downward, columns side by side within a page). Lines are broken at the column width and stacked down a column; when a line would overflow the column, flow moves to the next column, then to column 0 of the next page. Reuses next_line + place_line, so styles, per-run boxes, and discretionary hyphenation all carry over unchanged; the produced boxes feed Sel / hit-test / render directly. (Incremental multi-column reflow is a follow-on; an edit currently re-runs this full flow, which is fine at document scale.)

pub struct FlowImage { at: integer, mode: integer, w: float, h: float, id: integer }

An in-text (flow) image anchored at content char at, with a placement mode and size. mode: IMG_LEFT → float left, text wraps right; IMG_FULL → full-width block, text below; IMG_RIGHT → float right, text wraps left. Placed at the flow position when reached.

pub fn free_span(left: float, right: float, y: float, h: float, imgs: vector<PlacedImage>) -> (float, float, boolean)

free_span(left, right, y, h, imgs) → (flo, fhi, blocked): the WIDEST free horizontal interval in [left,right] over the vertical band [y, y+h), after removing every image rectangle that intersects that band. blocked is true when no free width remains (a full-width obstruction — the flow must drop below it). This is the one primitive both image kinds route through.

The seam for NON-RECTANGULAR page images (arbitrary shapes — deferred until the page-layout designer makes them usable): this function turns exclusions into a per-band covered interval [clo,chi]. A shape only has to report ITS covered interval at (y,h) instead of a rectangle's; the flow above never changes. So "shapes not just rects" is a change to the coverage query, not to the layout.

pub fn band_bottom(imgs: vector<PlacedImage>, y: float, h: float, left: float, right: float) -> float

the y to drop to when a band is fully blocked: the nearest bottom edge of an obstruction intersecting [y, y+h) (so at least one rectangle ends there, opening space). Always > y.

pub fn layout_flow_images(d: Doc, left: float, right: float, y0: float, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float, para_gap: float, line_h: float, page_imgs: vector<PlacedImage>, flow_imgs: vector<FlowImage>) -> Layout

Lay text into a single flow region [left,right] starting at top y0, steering around images. page_imgs are FIXED exclusion rectangles (page-layout images). flow_imgs are IN-TEXT images anchored at a content char, placed when the flow reaches them: IMG_FULL blocks the width (text resumes below), IMG_LEFT/IMG_RIGHT float to a side (text wraps in the free gap). Each line's width comes from free_span at its band, so text never overlaps an image; a fully-blocked band drops below the obstruction. line_h is the nominal line height used for the band test (exact for a uniform surface). Returns the text boxes + the placed images.

pub fn layout_page_columns(d: Doc, g: PageGeom, page_imgs: vector<PlacedImage>, flow_imgs: vector<FlowImage>, resolve: fn(integer) -> Style, measure: fn(text, Style) -> float) -> Layout

The full page: multi-column flow (paragraphs + hyphenation) that steers around BOTH image kinds. Page images are fixed absolute rectangles (each column avoids the ones over its x-range); in-text images are placed in whichever column the flow reaches their anchor in (IMG_LEFT/RIGHT float to that column's side, IMG_FULL blocks the column, breaking to the next column if it will not fit). The nominal line height is run_height(resolve(0)) — exact for a uniform surface. With no images this is identical to layout_columns; that is asserted.