Skip to content

Latest commit

 

History

History
503 lines (446 loc) · 31.8 KB

File metadata and controls

503 lines (446 loc) · 31.8 KB

Puri and the editor frame

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.

Ownership

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.

Measurement and placement

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.

Dispatch and hover

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.

Frame lifecycle

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.

Drawing and testing

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_bench

This 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.