Current implementation, 2026-09-04. This describes the code and its boundaries; it does not attribute the earlier framework comparisons or future plans to the owner. Those notes are retained in history.
Puri provides ephemeral widget descriptions, drawing, interaction helpers, and caller-owned state types. A description receives state and presentation inputs, then uses settled geometry to draw and register handlers. Puri holds no widget or application state between frames, retains no application hierarchy, mints no identity, and owns no global clipboard, window, clock, or focus service. Platform services arrive only as caller-supplied capabilities in the dispatch context. Puri grows only as Progred needs it.
Masonry is a quarry, not a foundation: Puri inherits none of its widget tree,
pods, or context protocol. Code taken from it is vendored with attribution and
purified in place; trivial widgets are rewritten instead. None is vendored
today: line editing borrows Parley's PlainEditor behavior, constructed
transiently for each pass and dispatch.
The caller owns focus, any focus order, and durable interaction state; a
description receives focus as an input. Durable helper state holds only
caller-owned content and cross-frame interaction data. LineEditState contains
text and cross-frame editing state; LineEditDescription supplies font, paint,
affixes, focus, placeholder, and chrome for this description, and its transient
handlers capture those presentation inputs. EditCtx supplies
mutable state, Parley contexts, a clipboard capability, and the host's command
modifier at dispatch. A WebAssembly host chooses that modifier from the browser's
platform rather than the compilation target. A focused editor
reports its caret as the frame's input area. The native shell allows platform
IME only while a frame has one and places composition candidates there; the
browser host does not enable IME yet.
The caller runs each EditOperation with that EditCtx rather than lending
the editor out to a handler. This lets the caller finish the state/service
borrows, then apply a document conversion and record undo as one interaction.
Puri does not know about those document operations. Progred captures conversion
in the current line handler, never in persistent selection state.
At Progred's line-control boundary, missing editing state has a defined default: the current spelling with the caret at its end. The frame uses that state without persisting it; dispatch materializes it on access for an editing interaction. Selection operations need not eagerly construct widget state. This policy belongs to the line control, not to generic selection or the data model.
The package boundaries are:
| Package | Responsibility |
|---|---|
| puri | Canvas and text vocabulary, placement geometry, hover claims, optional after-hover composition, typed handlers, pure widget descriptions |
| puri-widgets | Reusable composed widgets, including completion rows |
| measured | Box composition and ordered alternatives with opaque placement outputs |
| uig | Shared geometry vocabulary (Placement), re-exported by Puri |
| puri-vello | Native Vello canvas backend |
| puri-web | Browser Canvas2D fallback (WebGPU uses puri-vello) |
| Progred placement | Editor hover, navigation, popup policy, deferred paint, and dispatch inputs |
Reusable widgets do not interpret document values or choose domain completion
vocabulary. Progred and its projection libraries supply those choices. The Puri
runtime carries no widget catalog; puri-widgets depends only on Puri and stays
a pure consumer of it.
Puri owns Placement { rect, available_rect, clip_rect }, not a layout algebra.
Measurement composition, layout nodes, containers, and traversal belong to the
consumer. Puri text and editor descriptions expose metrics and accept a settled
placement; interaction helpers register against one.
rect is the widget's rectangle. available_rect is an optional expansion
offered by its container: rows offer their vertical span, columns their width,
and overlays both. Ordinary widgets ignore it. A fill_height combinator adopts
the available vertical span before invoking the child's placement continuation.
This does not change intrinsic measurement or trigger another choice search.
Available space is neither a clip nor a hit target.
clip_rect is the effective enclosing axis-aligned clip,
not already intersected with the widget. Ordinary children inherit the clip;
clipping containers intersect their bounds into it. Clipping travels only in
each placement, never as mutable context. Hover and gesture starts
must lie inside both rectangles. Motion and release for an active gesture can
continue outside them.
Canvas clips are separate: they constrain ink and can use arbitrary shapes. An axis-aligned layout clip neither describes nor replaces an arbitrary canvas clip. Clipping does not in itself remove navigation or active handlers.
Progred composes boxes by width, ascent, and descent. Rows align baselines,
top edges, or centers; top-aligned rows and columns choose a child's baseline.
Wrappers pad, overlay, or decorate the result. With ordered alternatives, this
is Progred's whole layout algebra; there is no general layout engine. Anchors
and clearance derive from the same metrics, outlines, and stroke widths used to
draw. Padding and gaps are explicit styling inputs, never offsets tuned to
compensate for borders or other geometry owned elsewhere.
measured::choices settles
ordered alternatives over already measured leaves. The first preferred form
whose natural width fits wins; otherwise the last form accommodates the
available width, unless an earlier form's natural width is no wider than that
accommodation, in which case the earlier form wins at its natural width. A form
chosen at its natural width places every nested alternative in its first form.
Selection does not reshape text or rerun projections.
Shared layout nodes belong to this one frame and are consumed by the selected
form.
The choice engine has no GID, editor, or Puri dependency. ChoiceBuild owns
per-frame sharing and choice bookkeeping; resolve_choices returns a
Measured<Out>: an extent and placement callback. The selected choice graph
places directly, without building a second measured container tree. Plain
measured combinators compose callbacks using the same geometry helpers, not a
Kind enum. ObserveLayout exposes the chosen row/column/overlay and direct-child
boundaries during that placement, for consumers that need structural traversal.
It does not expose discarded alternatives or require another tree. Wrappers do
not interpret their output. Out-of-flow
content uses attach: only the base contributes to surrounding width, and
the consumer supplies how the two settled subtrees place. Popover styling,
position, occlusion, and raising remain Progred policy.
The display builder's floating operation supplies only two boxes and a
positioning function. The ordinary popover widget
composes its padding, panel ink, and input blocking explicitly; the layout
interpreter does not add these to a floating box.
Native widgets use progred::display::widget::Widget: a measurement function
whose result feeds settled placements into a running HoverPass. Each leaf
answers hover immediately and contributes a continuation for after hover settles.
finish returns HoverOutput; binding its continuations produces paint and
handlers independently. There is no catalogue of ordinary hover callbacks. Puri
supplies generic AfterHover<H, O> composition,
without knowing a layout system or Progred's source identities.
LineEdit uses this path, with no control-specific layout constructor. Native
handlers receive the current settled hover as an
explicit dispatch input; the host suppresses that target outside its owning view.
CanvasSink is the object-safe primitive drawing interface implemented by
Vello, Canvas2D, and recorders; Canvas adds generic convenience methods.
Native render closures outlive measurement without fixing a rendering backend
or constructing GID drawing data.
Document-aware widgets live inside Progred. During preparation they borrow
the current sources, selection, view, and path, requesting document-site state
only when needed; inert decorators construct no editing state or capabilities.
Their handlers capture only
the props and location they need, receive &mut Editor at dispatch, and call
ordinary editing helpers. There is no per-widget
dictionary of editor callbacks. A read-only line installs no editing handlers.
Puri's text editor remains independent of these document operations.
The location-facing callbacks capture an
editing scope. At dispatch, an opaque borrowed
Access combines with that scope to create an Edit interface. Opening it
neither allocates nor copies the editor. The scope interprets locations with
an optional-path-returning Conject, not arbitrary replacements of every editor
operation; the normal projection uses its allocation-free identity case.
Selection and annotations remain local to the occurrence, while document reads
and mutations resolve through the same conject. Document-editing shell commands
use the scope retained by the selection too. Copy and fold are instead supplied
by the current projection, using its displayed value and fold default without
requiring a document source. A detached occurrence has no document source;
read-only behavior follows from that rather than a widget-specific write veto.
Ordinary decorations do not resolve paths or inspect selection.
A hover output is not a Canvas: it retains
whole-widget render continuations, then executes their draw calls directly after
hover settles, rather than allocating a deferred closure per drawing operation.
The native completion card uses those same outputs. Its rows draw directly
through CanvasSink; it never needs a document resolver or Grap interpreter.
The container combinators share scrolling
and out-of-flow placement over the running HoverPass. Layers supplies
clipping and floater attachment. Hover callbacks compose input handlers through
HasHandler. The editor adds view ownership separately. Ordinary probes run
immediately in painting order; only floating placements are queued, running
afterward, outside ancestor clips.
The navigation combinators supply
selectable leaf and whole-value stops. Progred consumes the box engine's
ObserveLayout traversal, folding each child into line summaries as it finishes:
columns concatenate, while rows align the declared logical baselines. See
navigation for how whole values and multiline blocks join lines.
No deferred neighbor providers, independent navigation axes, or pixel-distance
search remain. Puri does not know this policy; it still receives ordinary events
and handlers.
After placement, the temporary summaries resolve the selected occurrence's
four destinations. One ordinary Event::Navigate(direction) handler retains
their paths and arrives through each destination's landmark; no full graph or
logical layout survives in the installed frame. Each view has an independent collection. Raw controls and
editing handlers get first refusal of arrows, including modified arrows; only
an unhandled key becomes Navigate. The shell reveals the resulting selection
using existing landmarks.
Left/Right visit stops in logical reading order and wrap between lines. Up/Down
visit the first stop on the adjacent logical line. Arrival direction reaches
the destination widget: text uses beginning/end caret placement for Right/Left,
and ordinary defaults for vertical arrival. There is no directional history.
Landmarks separately retain geometry and direct selection behavior for
pointer/source selection and Select All. Stops through jump keep their editing
context; at results remain read-only.
The projection helpers now only declare
whole-value/leaf stops and compose common inline-or-indented head/body layouts.
They use the ordinary layout functions, with no navigation-specific row/column
wrappers. group(content) uses the generic widget::around placement boundary,
not a layout opcode. See layout navigation for the precise
logical-line policy, tests, and current limitations.
widget::before and widget::after contribute the same native outputs below
or above an arbitrary child. Their preparation functions capture current inputs,
then return a hover callback to run over settled placement; layout knows neither
the control nor its event policy. Click, activation, and picking are ordinary functions built
on this combinator. Hover claims, occlusion, and optional hover feedback are
ordinary decorators too. Generic Puri probes test settled hit geometry and
retention immediately inside that callback; the frame retains both the winning
claim and those probes for targeting later pointer input. The editor adds the
owning view. Insert and collapse handles request
feedback explicitly, not through a target-type switch in the interpreter.
Interaction wrappers use before, keeping child handlers in front of enclosing
handlers. after reverses that order when requested; borders use it to paint
above content without installing handlers. Only the chosen alternative invokes
its placement callbacks.
libraries::layout::on_event is the Grap adapter over that same interface.
It encodes events and installs one native handler that calls the
site-scoped interpreter directly. Ordinary native widgets do not touch
this interpreter. Layout has no Grap-event constructor or interpretation arm.
The layout builder interface belongs to
progred::display. Layout is a reusable program over that interface; its
production interpreter prepares the choice graph, while a test-only recorder
retains structure for inspection. There is no production layout enum.
Projection recursion uses ordinary preparation functions with an explicit
source scope, not path-bearing Layout variants. Native widgets and the editor
share one placement output; there is no editor-side Layout interpreter.
See layout and widget continuations for the complete
chain and the distinction between preparation and placement.
Pane sizing precedes content projection. Ordinary document panes scroll over content-sized output; explicit viewport panes pass their assigned size to a content function and clip its output without adding padding or scrolling. Both use the same placement, clipping, and handler contracts; viewport functions do not add a stretch/flex policy to the baseline layout algebra.
around lets a consumer control whether or when its subtree places; before and
decorate express ordinary placement/paint ordering, including ordinary
leading work, without special cases. These belong to the
consumer's layout composition, not to a Puri widget's return type.
Handler is one function over the input Event
enum. It receives mutable caller state and explicit dispatch inputs, returning
acceptance plus any unconsumed event. over tries the later contribution first
and passes its remainder to the earlier contribution. Scroll carries an ordered
batch of original packets, including positions, timestamps, modifiers, and units.
on_scroll_batch receives that batch intact. A handler may sum it, apply
acceleration, or use the sample-wise on_scroll adapter. Partial consumption
forwards the ordered unconsumed packets, with adjusted deltas where necessary;
acceptance survives even when the next handler declines. Clipping divides a
batch only at crossings of its bounds, retaining in-bounds runs as batches.
Native pointer gestures use the same ordered-batch contract through
Event::Gesture, on_gesture_batch, and the sample-wise on_gesture adapter.
interact::on_pinch adds placement/clipping checks; widgets own the zoom policy.
Pinch deltas are fractional scale changes, not pixels or scroll distances.
Scroll containers also emit read-only capture probes with their settled placement,
scale, offset, and limits. The web embed's wheel=auto listener consults these
before allowing winit to receive the event: any consumable scroll captures the
whole event, otherwise the browser keeps it. Probes reuse the offset/clamping
calculation and ancestor clips; normal handlers still choose the frontmost
consumer and propagate internal remainders. Probes are snapshots of the installed
frame, so a batch can briefly continue capturing after reaching an edge, until
its successor is installed. They do not dispatch handlers, mutate state, or force
a build. Native dispatch ignores this output. Custom wheel actions such as camera
zoom remain available in the full editor's unconditional wheel=editor mode.
The typed helpers are ordinary combinators over this interface.
Widgets test their own geometry; Puri does not infer acceptance from state changes.
Progred activation, picking, and raw pointer-down handlers use that same front-to-back chain, not separate dispatch phases. The dispatch context supplies the settled hover target, its owning view, and reveal geometry explicitly for every event, including motion, scroll, release, and IME. View wrappers hide another view's hover without blocking input needed by active gestures. Event acceptance controls propagation; it does not tell the shell to infer a domain action or gesture. The accepting handler performs the action and installs any continuation. Gesture startup is an explicit update to caller-owned state: selection and drag startup compose in the same accepted interaction, rather than a gesture being inferred from an unrelated handler returning true.
A hover callback returns its claim plus independent paint and event outputs.
A puri::hover::Claim either names a target, directly or by retention, or
occludes: the claim analog of an opaque fill.
Callbacks run over settled geometry in painting order; a later direct claim or
occluder supersedes an earlier claim, while retention cannot displace a direct
claim. Occlusion also consumes
pointer starts, while active motion/release can still reach their handlers.
A floating card carries its owning view even when it covers another pane.
Hover is derived for each pass. The frame is built without a resolved hover
input; deferred paint receives the answer after the hover continuation runs. LazyPointer is
an input-side dead-zone filter for small gaps. Pressed interactions retain the
anchor they need as explicit caller-owned gesture state. While a press holds
the prior hover, its owning view is retained too, both when probing installed
geometry and when building a successor. Release resumes normal probing.
The application shell adapts Winit events and supplies
platform capabilities. Each window owns an EditorRunner: the mutable Editor
beside its saved FrameState (dispatch, settled hover, and pending painting).
Widget handlers receive only &mut Editor, not their own saved dispatch. The
runner borrows those separate fields directly and owns the input batches and
frame replacement. Window setup builds the initial frame; reset uses a no-op
dispatch until the successor is built.
HoverChanged and ModifiersChanged use the ordinary Puri event chain, with
the settled hover supplied in the caller's dispatch context. Progred emits a
hover notification when the target or its owning view changes. Pointer motion
first probes the installed frame's geometry, allowing the notification to run
before building a successor. It uses the same probes and precedence as
placement, not navigation rectangles or a second approximate hover algorithm.
The successor still computes its own hover from fresh geometry. An accepted
notification builds one successor; any further hover reaction waits for an
actual paint/submission before continuing. Oscillating reactions yield across
painted frames rather than recursively dispatching, panicking, or reaching an
iteration cutoff. The runner remembers the last notified identity and the paint
boundary, not a queue of handlers from discarded frames. Intervening input can
replace an unpainted successor, and the notification uses the current frame.
See the scheduling contract.
Placement runs hover contributions over settled geometry, then the resolved
hover binds independent handlers and deferred painting. Paint is not required
to produce the handlers. frame builds and installs
the frame; input interprets input into its successor.
See layout continuations for the phase boundaries.
A changed frame input remints a whole frame. The event-to-redraw pending frame stages the already-built successor for presentation; it avoids building it again at redraw. There is no event-specific list of changes considered irrelevant to rendering, and no partial invalidation. Explicit library computations may reuse results through the caller-owned dependency graph; frame construction still runs. If whole frames become too slow, the remedy is one general dependency-tracked invalidation system, observing missing definitions and ordered definition sets too, not event-specific special cases.
Pointer motion, pressed or unpressed, accumulates until a redraw or discrete
event. Each dispatch receives one PointerUpdate: coalesced contains earlier
observed states in order, excluding current, which is the latest state.
Only the latest packet's predictions survive; predictions are never applied as
observed input. Different contacts, buttons, modifiers, scales, or viewport sizes
start a new batch. Release and cancellation flush pending motion first.
The editor holds at most one active projection gesture, in a caller-owned
slot. The accepting handler installs it, finishing any predecessor, and it owns
motion until it finishes. Domain updates and undo grouping live in the gesture,
outside platform dispatch.
An active projection gesture exposes advance and finish. The shell finishes
it on release, cancellation, replacement, history restoration, or a successful
save. Its implementation owns any finalization; the frame pipeline has no
number-specific presentation channel. The number library's scrub widget keeps
precision-aware spelling in the selected line editor during the gesture and
clears that editor on finish, leaving other selection payload fields intact.
Value-changing gestures use a concrete edit run holding the target and undo
grouping flag, not a dictionary of editor callbacks.
Handlers choose which samples matter, without rebuilding between samples.
Batching is not a per-widget opt-in, and the shell never discards earlier
samples. Number scrubbing integrates the full precision path; state-drag
callbacks receive the latest logical displacement and the earlier
displacements, letting Fidget orbit use only the latest. Raw Puri handlers
receive the whole pointer update;
Grap motion events expose earlier sample records under coalesced alongside
their existing latest-position fields. Scroll also reaches the handler as one
batch before a single successor-frame build; the shell never sums or replays it.
Sample-wise controls read current caller-owned state so successive samples do
not overwrite each other. Grap scroll events expose the packet records as a list
under content. Unclaimed touch motion produces a scroll batch from the observed
position differences, preserving reversals and sample metadata.
Pinch/rotation samples also wait for the redraw boundary, interleaving with
pointer refreshes without forcing intermediate builds. Switching between scroll
and gesture input flushes the earlier batch; gesture end/cancel also flushes.
Grap handlers receive gesture events with sample records under content;
each names pinch (fractional scale) or rotation (clockwise radians) and delta.
Puri's timer capability creates a one-shot handle and a weak completion handle for the shell. Dropping the owning handle cancels it; delivery clears its deadline and cannot affect a replacement handle. Progred retains only the weak completions, wakes at the earliest deadline through the event loop, delivers due timers, and requests a frame. Early platform wakeups leave future deadlines pending. Timer queues have document lifetime and retain no document callbacks.
puri-widgets supplies a caller-owned debounce over that capability: triggering
replaces its timer, and readiness means either delivery or elapsed monotonic
time. CAM camera handlers use it only for accepted zoom changes, with a 150 ms
delay. Each preview derives readiness at frame build time, so a missing delivery
recovers on the next frame. Only implicit pixel-job submission waits; input,
immediate mesh drawing, and other previews continue independently. The shell
knows nothing about scrolling, debounce durations, or render admission.
Leaving a window clears its hover position, not its active drag. Captured motion and release retain their unbounded coordinates. Focus loss cancels through the same pointer-cancellation handlers and clears the adapter's pressed state. Window focus is a separate frame input from document selection. Losing focus clears selection and its transient editing state, including completion queries and uncommitted composition. Document edits already made remain; refocusing does not restore the old selection. Primary and related selection highlights disappear. Keyboard and IME input require focus. In browser embeds, canvas focus and the iframe document's focus must both be present; standard window focus/blur events cover leaving the iframe while its canvas remains the active element. Loading an embed does not focus it automatically.
Cross-frame reuse consists of caller-threaded text shaping and the explicit caller-owned computation graph, currently used by CAM geometry. Visible Grap canvas programs record once per frame, sharing commands and source hits between hit-testing and painting. This within-frame sharing and the layout DAG do not reuse computation to construct later frames. The installed frame retains its hit tests, including recorded drawing shapes, alongside its handlers for subsequent input targeting. Replacement drops both. Neither Puri nor layout owns the computation graph or decides which library results to retain, and neither keeps a hidden cache.
Canvas is a drawing interface over shapes, brushes, glyph runs, images, meshes, and
scoped clips. Production code can draw into Vello or Canvas2D; DrawList
records the same operations for inspection and replay. Rectangles remain
rectangles in recordings rather than becoming paths merely for transport.
Parley owns text shaping. Puri does not depend on a platform clipboard library.
The native canvas now groups contiguous vector operations into Vello scenes, interleaved with independent image textures and mesh viewports in painting order. This policy belongs to the compositor, not panes, widgets, or layout. Vector-only frames keep a single Vello call. Mixed clipping scopes preserve group coverage: integer rectangles use scissoring, while other clips use an intermediate group and a Vello-rendered mask. Draw-image operations do not enter Vello's image atlas; image brushes still do.
Each window owns its uploaded-image resources, keyed by immutable image blob
identity, dimensions, and format. Unchanged uploads are reused; absent images
are released on the next paint. Scratch textures are resized and reused, with
masks and clip-group textures retained only when used by the current frame.
These are graphics resources, not cached projections or computations. The
device's compositor is shared across windows, without sharing their resource
lifetimes. draw_mesh carries shared triangle geometry, an orthographic view,
and optionally depth-image replacement of a draft surface. The native compositor
draws it on the same device and consumes the resulting premultiplied texture
without a CPU round trip. Its renderer retains one mesh upload and surface
color/depth upload, not evaluated widgets or raster results. DrawList and the
initial Drawing representation retain the mesh operation; the default canvas
interpretation rasterizes it on the CPU for browser/export consumers.
See image composition measurements
and direct mesh measurements.
Text leaves may request subscript typography: Puri shapes a smaller font and reports ascent/descent relative to the surrounding baseline. Ordinary rows then align it correctly without a new layout operation. The number projections use this for muted representation labels outside the editable digits.
Puri's delimiter widget exposes its advance and minimum span for measurement,
then draws its stretched outline directly into the canvas at paint time. It
does not build an intermediate drawing-command list; clipped or skipped paint
does not construct a path. Its width is fixed by text size; only the vertical
shape stretches. Progred's bracket combines two ordinary
fixed-width widgets and a child in a row, with fill_height on the sides.
There is no Surround operation, maximum-width reservation, or child-dependent
remeasurement. selectable_bracket explicitly composes selectable_widget
inside the stretching wrapper, so its handlers use the expanded rectangle too.
Structural cell/list/record and expression projections opt into that behavior;
Grap's low-level bracket layout is inert unless explicitly wrapped.
puri-widgets::panel supplies the common fill/border painter for popup cards
and projection borders. Colors, stroke, radius, placement, paint order, and
whether the surface blocks input are supplied by the host. Panel drawing owns
no child layout, popup policy, or document state.
Color controls likewise paint directly through CanvasSink: their gradients,
checkerboard, swatch, and markers are not intermediate command lists. Progred
supplies the fixed metrics, scale, and point handlers. The paint-only
widget::paint combinator attaches one deferred painter to an extent, without
hover or document access; delimiters and Fidget viewport leaves use it too.
Synchronous implicit previews render during projection, then submit an image at
paint time. Mesh views prepare shared geometry and defer rasterization to the
canvas backend. CAM computation graphs still own model/stock meshing and async
implicit work; camera and interaction policy remain in Progred.
The Drawing description remains for explicitly stored drawing data decoded
by the layout library. Its interpreter is not used by these native widgets.
puri-widgets::text_frame measures empty frames using the caller's font and
supplies the outline geometry used around text. The ordinary widget::empty
function and slot combinator use it, as do pending values and their selection
outlines; there is no empty-slot layout opcode. Completion behavior and the meaning of an empty slot remain
in Progred; the widget owns only metrics and drawing.
Tests can drive pure descriptions and handlers without a window. Projection
fixtures, interaction regressions, SVG export, and profiling live in
projection/tests. SVG tests are a
focused filter rather than the home of all interaction regressions:
./tools/sandbox-cargo test -p progred svg_benchThis writes sample SVG files under target/. The ignored IoP profiling loops
are in frame/profile.rs.
See build security for sandboxed commands. Agents do not
launch the application; the owner performs visual testing.