Expand description
§Exponential UI core (VAPP-86)
The engine every native Exponential UI painter (gpui, SwiftUI, Compose)
runs on: an A2UI surface comes in as messages, the
reducer turns the flat component list into ONE normalized tree in the
core vocabulary (the A2UI basic catalog mapped onto it, macros expanded
through the generated table, extension catalogs validated and their
natives passed through as Extension nodes), the theme resolves every
$token reference and per-part recipe into concrete visuals, taffy
lays the tree out against the surface width with a batched two-phase text
measurement that crosses the FFI at most three times per pass (sizes AND
first baselines), overlay layers (dialogs, drawers, popovers,
tooltips, menus and submenus, the Select / picker popups, the toast band)
get their own roots placed against the viewport, an anchor or a point,
and windowed Lists / Tables lay out only the visible rows. Nodes live
in stable slots reconciled by id, so a data write, a tab switch or a
scroll touches only what changed (NodeDelta). The painter receives flat
PlacedNodes with RESOLVED visuals and paints; it never reads the
catalog or the theme.
Round 1 (packages/exponential-ui/docs/round-1-contract.md): bound macro
inputs evaluate at bind time (data::bind_tree, data::run_action),
style conditions (conditions), the new style keys, responsive props,
the locale / strings / code / chart / a11y contracts (locale,
strings, code, chart, a11y), surface settings (mode incl.
system, density, contrast, font scale, safe-area insets).
Round 2 (docs/round-2-contract.md): templates LIFTED out of the tree
(ReduceResult::templates), #<index> keys and accumulated instance
suffixes, a host format::Formatter (default: the English fallback),
display strings, per-node direction and physical text alignment
(direction), keyframe animations (animation), the Resizable
arithmetic (resizable) and native, one-axis windowing with sections,
sticky headers, unbounded lists and scrollToIndex (list),
position: sticky, backdropBlur and the shared layout numbers
(layout).
The TypeScript reference implementation lives in packages/exponential-ui
(@exponential_app/eui); both replay the same fixtures byte for byte, and
the catalog, macro table and built-in themes are embedded here from the
same generator (src/generated).
The coarse mobile facade is the sibling crate exponential-ui-ffi.
Re-exports§
pub use catalog::is_known_token;pub use catalog::parse_token_ref;pub use catalog::CatalogView;pub use catalog::A2UI_BASIC_CATALOG_ID;pub use catalog::CORE_CATALOG_ID;pub use catalog::CORE_LITE_CATALOG_ID;pub use catalog::UNKNOWN_COMPONENT;pub use taffy;pub use types::*;
Modules§
- a11y
- Round 1 (docs/round-1-contract.md §6): the accessibility contract as data
(
catalog/a11y.json, embedded): per component its role and keyboard expectations, the rules every component keeps, and the commands a host may send a surface (surface::SurfaceCommand). Mirrorssrc/a11y.ts. - animation
- Round 2 (docs/round-2-contract.md §2, animation): the token-only
animationstyle key.catalog/style.jsonanimationsholds the keyframe sets; this module turns one into what a painter needs at a moment — the duration from the theme’s motion tokens, per-segment easing, the channel values (opacity, translate, rotate, scale, the shimmer band) — and into CSS@keyframes. Mirrorssrc/animation.ts;fixtures/animations.jsonlocks the sampled frames. - basic_
map - The A2UI basic catalog → core catalog map, applied by the reducer. The
declarative part is
catalog/basic-map.json(embedded); the NAMED transforms listed under itstransformsare implemented here, one function per name. Mirrorssrc/basic-map.ts. - bench
- A synthetic ~n-node surface for the timing measurement (the VAPP-4 bench tree in the core vocabulary): cards in a grid with headings, text, pills and buttons, so a pass exercises flex, grid, wrapping and controls.
- catalog
- The catalog as data:
core.catalog.json(embedded by the generator), the catalog ids, token groups, and the merged VIEW of the core plus registered extensions every lookup goes through. Mirrorssrc/catalog.ts. - chart
- Round 1 (docs/round-1-contract.md §3, Chart): the numbers every painter
must agree on — the value extent per kind and the “nice” axis ticks
(Heckbert’s nice numbers), the series colour order and the donut hole.
Pure; geometry (bars, arcs) stays each painter’s. Mirrors
src/chart.ts. - code
- Round 1 (docs/round-1-contract.md §3): CodeBlock’s BUILT-IN tokenizer, a
line-by-line port of
src/code.tsovercatalog/code.json(embedded). One forward scan, first matching rule wins; the tokens split into LINES and adjacent tokens of one kind merge.fixtures/code-tokens.jsonlocks it. The scan walkschars where the TS walks UTF-16 units; the merge step makes the two agree for any text (an astral char is one plain token either way). - conditions
- Round 1 (docs/round-1-contract.md §2): STYLE CONDITIONS — the one media
grammar (
catalog/style.jsonconditions.media, hand-parsed: no regex engine), the state keys, andresolveConditions, the reference flattening ofsrc/style.ts: the base keys, then every MATCHING@mediablock in SOURCE order (later wins), then:hover,:focus-visible,:pressedin that order (pressed wins). Sizes are the SURFACE box;min-*= size >= N,max-*= size < N (strict); a$breakpoint.<name>resolves through the context’s breakpoints (an unknown token never matches); orientation portrait = height >= width (landscape when the height is unknown).fixtures/style-conditions.jsonlocks it. - data
- The surface DATA MODEL and A2UI’s dynamic values: a prop may be a
{path}binding (a JSON Pointer into the model, relative inside a template item) or a{call, args}client function;resolve_valueturns a prop tree into literals for one pass. Round 1 (docs/round-1-contract.md §1): the BIND pass ofsrc/dynamic.ts—bind_tree(props resolved along their schema with DATA props verbatim, styles, recipe props andaccessibilityresolved at any depth, falsyvisibledropped,$string.<id>through the surface’s table, row-scoped slots left forbind_row_slot) andrun_action(evaluate, thenset, then the event);fixtures/bind-time.jsonlocks both. The function table = the core functions (expr::core_function) + the basic catalog’sand/or/not/requiredwith the core truthiness (0 is TRUE) + the basic validators and formatters. - direction
- Round 2 (docs/round-2-contract.md §2, direction):
directionmay sit on ANY node and is inherited; everything direction-dependent reads the NODE’s resolved direction: flex row order, logical insets and paddings, mirrored glyphs andtextAlignstart/end (the implicit alignment of text isstart). Mirrorssrc/direction.ts;fixtures/text-direction.jsonlocks it. The surface carries the result intocrate::style::Visual(direction,text_align) so painters shape text with the node’s direction as the bidi paragraph direction. - engine
- The layout ENGINE: taffy’s algorithms (flexbox, grid, block) over the
surface’s own node slots (taffy’s “custom tree” API), so the core
controls what
TaffyTreehides: - expr
- The macro template’s value language (
catalog/macros.json$comment):{props.x}paths with|fallback,len(path),range(n),!path, string interpolation, and the$map/$cond/$eq/$lt/$add/$sub/$clamp/$percent/$coalesce/$text/$not/$fillvalue objects. Mirrorssrc/expr.tsline by line. - extension
- Extension catalogs: their own catalog id,
extendsthe core, their components (natives a host paints, macros expanded from their own templates) and no name that shadows a core one. Mirrorssrc/extension.ts. - format
- Round 2 (docs/round-2-contract.md §3): FORMATTING through a host-supplied
Formatter. Every number, currency, percent, date and relative time a surface shows (Table cells, NumberField, chart ticks, calendar names, theformat*bind functions) goes through ONE formatter per surface, in the surface’slocaleand the host’s time zone. The default isEnglishFormatter(catalog/locale.jsonenglishFallback, = Intl en-US, instants in UTC);ZonedEnglishFormatter= the same at the host’s UTC offset (the core has no zone database: the zone reaches it ONLY through the formatter); an FFI host passes its own (Foundation,android.icu). - generated
- The generated constants, COPIED here by
packages/exponential-ui/scripts/generate.ts(bun run --filter @exponential_app/eui generate) so the crate packages standalone;tests/generated_drift.rsfails when the copies and the package’sgenerated/*.rsdiffer. Never edit by hand. - geometry
- The MEASURE contract a painter (built in or a host’s override) must keep
for a control (VAPP-92; mirrors
src/geometry.ts): its box comes from the theme’s tokens through the recipe of the part that IS the control, never from the painter.control-geometry.jsonrecords every built-in theme × control × size;check_geometryis what the conformance suite runs an override through. - hold
- VAPP-162 (docs/round-5-contract.md §9): the Button
holdgesture as a pure state machine every painter shares (the core drives it throughSurface::hold; gpui may also run it directly). Withhold: truea press never firespress: pointer down (or Space / Enter key down, key repeat ignored) starts the hold (holdStart, thepressedstate, theholdingLabel); release withinHOLD_SLOPpx of the box (or key up of the key that started it) ends it (holdEnd); the pointer leaving the box by more than the slop, a pointer cancel, Escape or losing focus cancel it (holdCancel); after a cancel nothing fires until the next down.fixtures/interactions-round1.jsonbuttonHoldlocks it. - host
- The host API (VAPP-91)
- json
- JSON helpers shared by the reducer, the expander and the theme resolver: the JavaScript number model (one double; integral values print without a fraction) and a canonical form for byte-for-byte fixture comparisons.
- layout
- Round 2 (docs/round-2-contract.md §7): the shared layout NUMBERS
(
catalog/layout.json, generated asLAYOUT_CONSTANT_NAMES/VALUES): one place for the core and every painter. - layout_
tree - The LAYOUT tree: the normalized
UiNodetree (macros already expanded) becomes a flat pre-order list of layout nodes the engine and the painters work on. Here the core adds what painters must not invent: the BIND pass (bindings, calls and$string.<id>copy resolved in props, styles and recipe props; a node whosevisibleresolves falsy is dropped with its subtree), template children instantiated per item (ids<template>.<key>with the templatekey, else<template>.<i>), natives with structure of their own get synthetic PART nodes (ids<owner>.<part>, recipes keyed on the owner’s component so a theme styles them), overlay natives and every popup (Select, the pickers, menus and submenus, tooltips, hover cards, toasts) put their content into LAYERS with their own roots, and extension natives arrive asExtensionleaves. - limits
- VAPP-103: the input LIMITS (
catalog/limits.json, generated asLIMIT_NAMES/LIMIT_VALUES): an agent’s input past one is refused with an issue, never worked on. Mirrorssrc/limits.ts; the issue messages are byte-identical in every core. - list
- The WINDOWED
List: the core lays out only the visible range plus overscan, reports the content extent, and memoizes item extents per key so a scroll step is constant work. - locale
- Round 1 (docs/round-1-contract.md §4): the locale decisions every renderer
shares (
catalog/locale.json, embedded): the week start by region, the likely region of a bare language, the right-to-left languages. Formatting itself is the platform’s ICU; these are the parts that must not differ. Mirrorssrc/locale.ts. - macros
- The macro expander: applies
catalog/macros.json(plus extension macros) to a normalized tree until every node is native. Mirrorssrc/macros.ts;fixtures/catalog-macros.jsonlocks the output. - measure
- The host MEASURE contract. The VAPP-4 verdict: never one FFI upcall per
taffy measure call (taffy asks a leaf 10–15 times per pass). The core
batches: ONE upcall for the intrinsic widths of every dirty leaf (with the
resolved text style, so Compose can pick the font before its first pass),
taffy runs against a Rust-side memo, ONE upcall for the heights at the
widths taffy decided, and a third round only when a height changed a width
decision (counted in
LayoutOutput::measure_rounds). In-process painters (gpui) may answer per request throughDirectMeasure; nothing crosses an FFI there. - overlay
- The OVERLAY placement rule every renderer keeps (
src/overlay.tsin the TS package;fixtures/overlay-geometry.jsonlocks it): an anchor box, the overlay’s size, the viewport and a preferred side → where it lands.OVERLAY_OFFSETpx from the anchor on the preferred side, centred on the cross axis; FLIP when the preferred side has less room than needed and the opposite has more; then SHIFT on the cross axis to stayOVERLAY_PADDINGinside the viewport. - qr_code
- VAPP-162: the QrCode native’s MATRIX (catalog/style.json
qrCode). A faithful Rust port of Project Nayuki’s QR Code generator library (https://www.nayuki.io/page/qr-code-generator-library, MIT licence, Copyright (c) Project Nayuki; the notice is in docs/third-party-licences.md and packages/exponential-ui/vendor/nayuki-qr/LICENSE.txt), mirroring the TS referencesrc/qr-code.ts: ONE byte-mode segment over the value’s UTF-8 bytes, the smallest version 1–40 that fits, the given error correction level kept exact (noboostEcl), the mask with the lowest penalty.fixtures/qr-code.jsonlocks every matrix bit. - recipes
- The recipe CONTRACT as code (VAPP-92): which parts each component has and
which props a theme’s
whenmay key on. Natives are listed by hand incatalog/recipes.json; macro parts are derived from their templates inmacros.json(everypart, the macro’srecipeProps+ each part’s$recipekeys), so the two files cannot disagree. Mirrorssrc/recipes.ts. - reducer
- The reducer: a surface’s flat component list (A2UI
updateComponents, core OR basic catalog OR an extension) → ONE normalized tree in the core vocabulary, macros expanded, every unknown component the explicitUnknownplaceholder. Mirrorssrc/reducer.ts; the fixtures lock it. - resizable
- Round 2 (docs/round-2-contract.md §1, Resizable): the panel arithmetic
every renderer shares, so a drag or a key press gives the same sizes
everywhere. Sizes are PERCENTAGES of the group’s main axis minus its
handles (they sum to 100); only the two panels beside the moved handle
change. Results are rounded to 1e-6. Mirrors
src/resizable.ts;fixtures/resizable.jsonlocks it. - segmented
- VAPP-162 (docs/round-5-contract.md §3): Segmented
orientation: verticalandoption.count. A verticalbaris THE navigation rail: a column of fixed items (item = the rail’s width, 72 in every built-in theme, ×$control.tabBar), laid out by the core (layout_tree). Abaritem’scountis a badge at its icon’s top-end corner. - strings
- Round 1 (docs/round-1-contract.md §4): the BUILT-IN UI strings — the copy
no author writes (search placeholder, month buttons, copy, close, …). The
ids and English defaults are
catalog/strings.json(embedded); a host overrides any id per surface;$string.<id>in a prop value resolves through the surface’s table at bind time. Mirrorssrc/strings.ts. - style
- Style RESOLUTION for layout: a node’s style object (token references
already resolved by the theme, conditions still nested) → the flat map for
this pass (
conditions::resolve_conditions: media against the SURFACE box, the state keys against the node’s states) → logical properties made physical from the surface direction → a taffyStyleplus the paintedVisual. - style_
check - The
Boxstyle whitelist as a validator:validateStyleofsrc/style.ts, keyed off the embeddedcatalog/style.json. The regexes of the TS are small hand-written parsers here (no regex engine). - surface
- A
Surface= one A2UI surface: its catalog, its normalized tree, its data model, its theme and settings, the local UI state the core owns, and one layout engine (main root + one root per open layer) kept across passes. - text_
shimmer - EXP-1324: the Text
shimmercolours (catalog/style.jsontextShimmer), a port ofsrc/text-shimmer.ts;fixtures/text-shimmer.jsonlocks it. A painter fills a shimmering text’s glyphs with the gradient the style’s$commentdescribes, from these colours; reduced motion = plain text. - theme
- The theme loader and resolver (VAPP-92), a line-by-line port of
src/theme.ts+src/theme-types.ts. A theme is data, never code: one JSON file of token VALUES per mode plus component RECIPES.validate_themeturns a theme file into readable issues (never a panic),load_themeflattens itsextendschain into aResolvedThemeandresolve_recipeanswers a painter’s question for one mode with concrete values. - themes
- The built-in themes, compiled in (VAPP-92; mirrors
src/themes.ts).neutralis stock shadcn and the ROOT the others extend;exponentialis generated from the app’s design tokens;playfulis the deliberately different test theme;orangeisexponentialwith an orange primary (colours only, the brand reference). The generator embeds them already RESOLVED (BUILTIN_THEME_JSON), so they are parsed once, never re-flattened. - tracks
- Hand parser for the grid strings the whitelist admits (from the VAPP-4
spike):
gridTemplateColumns/Rows=<n>px | <n> | <n>% | <n>fr | auto | min-content | max-content | minmax(a, b) | repeat(n|auto-fill|auto-fit, …),gridTemplateAreas= string rows,gridArea= an area name,gridColumn/gridRow=a,a / b,span n, a name. - tree_
guides - Round 3 (VAPP-102,
docs/round-3-contract.md§2): the tree connector rule the CORE computes for nested rows, mirroringpackages/exponential-ui/src/tree-guides.tsline by line.tree_guidesis the ×4 rule of the app (packages/ui/src/tree-guides.ts, desktopdomain::tree_guides, iOSTreeGuides.swift, AndroidTreeGuides.kt), copied verbatim so a Section of Rows connects like a session tree or a PR stack.apply_tree_guidesis the ONE post-expansion pass (the end ofcrate::macros::expand_macros): over every node’s children in order, each Row root contributes its numericrecipe.props.depth(anything else counts as 0 and ends every subtree), and the rule fills the Row’sguidespart (a TreeGuides native) withelbowAt/tee/passThrough. Locked byfixtures/tree-guides.jsonand the Row cases ofcatalog-macros.json. - types
- The shapes shared by every module: the catalog source (
core.catalog.json), a normalized surface node, the nested authoring form, the A2UI wire form and an extension catalog. Mirrorspackages/exponential-ui/src/types.ts. - validate
- Props validation against a component definition — the catalog’s own mini
schema (
PropSchema), so the reducer, the fixtures and the generator agree without a JSON Schema engine. Mirrorssrc/validate.ts. - voice_
wave - NEW-77: the VoiceWave rings (catalog/style.json
voiceWave), a port ofsrc/voice-wave.ts;fixtures/voice-wave.jsonlocks it. A painter fills and strokes each ring’s closed path with its part colour (outer,mid,inner) at the ring’s alphas and centres the child atchild_size. - zoom
- EXP-1586: the ZoomableImage native’s maths (catalog/style.json
zoomableImage, whose $comment is the spec). Pure: every painter feeds its pointer, wheel, pinch and key input through these functions, so a zoom, a pan or a double tap lands on the same view everywhere. Mirrorssrc/zoom.ts;fixtures/zoom.jsonlocks it.