Skip to main content

Crate eui_core

Crate eui_core 

Source
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). Mirrors src/a11y.ts.
animation
Round 2 (docs/round-2-contract.md §2, animation): the token-only animation style key. catalog/style.json animations holds 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. Mirrors src/animation.ts; fixtures/animations.json locks 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 its transforms are implemented here, one function per name. Mirrors src/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. Mirrors src/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.ts over catalog/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.json locks it. The scan walks chars 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.json conditions.media, hand-parsed: no regex engine), the state keys, and resolveConditions, the reference flattening of src/style.ts: the base keys, then every MATCHING @media block in SOURCE order (later wins), then :hover, :focus-visible, :pressed in 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.json locks 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_value turns a prop tree into literals for one pass. Round 1 (docs/round-1-contract.md §1): the BIND pass of src/dynamic.ts — bind_tree (props resolved along their schema with DATA props verbatim, styles, recipe props and accessibility resolved at any depth, falsy visible dropped, $string.<id> through the surface’s table, row-scoped slots left for bind_row_slot) and run_action (evaluate, then set, then the event); fixtures/bind-time.json locks both. The function table = the core functions (expr::core_function) + the basic catalog’s and/or/not/required with the core truthiness (0 is TRUE) + the basic validators and formatters.
direction
Round 2 (docs/round-2-contract.md §2, direction): direction may 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 and textAlign start/end (the implicit alignment of text is start). Mirrors src/direction.ts; fixtures/text-direction.json locks it. The surface carries the result into crate::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 TaffyTree hides:
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 / $fill value objects. Mirrors src/expr.ts line by line.
extension
Extension catalogs: their own catalog id, extends the core, their components (natives a host paints, macros expanded from their own templates) and no name that shadows a core one. Mirrors src/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, the format* bind functions) goes through ONE formatter per surface, in the surface’s locale and the host’s time zone. The default is EnglishFormatter (catalog/locale.json englishFallback, = 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.rs fails when the copies and the package’s generated/*.rs differ. 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.json records every built-in theme × control × size; check_geometry is what the conformance suite runs an override through.
hold
VAPP-162 (docs/round-5-contract.md §9): the Button hold gesture as a pure state machine every painter shares (the core drives it through Surface::hold; gpui may also run it directly). With hold: true a press never fires press: pointer down (or Space / Enter key down, key repeat ignored) starts the hold (holdStart, the pressed state, the holdingLabel); release within HOLD_SLOP px 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.json buttonHold locks 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 as LAYOUT_CONSTANT_NAMES/VALUES): one place for the core and every painter.
layout_tree
The LAYOUT tree: the normalized UiNode tree (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 whose visible resolves falsy is dropped with its subtree), template children instantiated per item (ids <template>.<key> with the template key, 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 as Extension leaves.
limits
VAPP-103: the input LIMITS (catalog/limits.json, generated as LIMIT_NAMES/LIMIT_VALUES): an agent’s input past one is refused with an issue, never worked on. Mirrors src/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. Mirrors src/locale.ts.
macros
The macro expander: applies catalog/macros.json (plus extension macros) to a normalized tree until every node is native. Mirrors src/macros.ts; fixtures/catalog-macros.json locks 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 through DirectMeasure; nothing crosses an FFI there.
overlay
The OVERLAY placement rule every renderer keeps (src/overlay.ts in the TS package; fixtures/overlay-geometry.json locks it): an anchor box, the overlay’s size, the viewport and a preferred side → where it lands. OVERLAY_OFFSET px 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 stay OVERLAY_PADDING inside 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 reference src/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 (no boostEcl), the mask with the lowest penalty. fixtures/qr-code.json locks every matrix bit.
recipes
The recipe CONTRACT as code (VAPP-92): which parts each component has and which props a theme’s when may key on. Natives are listed by hand in catalog/recipes.json; macro parts are derived from their templates in macros.json (every part, the macro’s recipeProps + each part’s $recipe keys), so the two files cannot disagree. Mirrors src/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 explicit Unknown placeholder. Mirrors src/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.json locks it.
segmented
VAPP-162 (docs/round-5-contract.md §3): Segmented orientation: vertical and option.count. A vertical bar is 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). A bar item’s count is 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. Mirrors src/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 taffy Style plus the painted Visual.
style_check
The Box style whitelist as a validator: validateStyle of src/style.ts, keyed off the embedded catalog/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 shimmer colours (catalog/style.json textShimmer), a port of src/text-shimmer.ts; fixtures/text-shimmer.json locks it. A painter fills a shimmering text’s glyphs with the gradient the style’s $comment describes, 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_theme turns a theme file into readable issues (never a panic), load_theme flattens its extends chain into a ResolvedTheme and resolve_recipe answers a painter’s question for one mode with concrete values.
themes
The built-in themes, compiled in (VAPP-92; mirrors src/themes.ts). neutral is stock shadcn and the ROOT the others extend; exponential is generated from the app’s design tokens; playful is the deliberately different test theme; orange is exponential with 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, mirroring packages/exponential-ui/src/tree-guides.ts line by line. tree_guides is the ×4 rule of the app (packages/ui/src/tree-guides.ts, desktop domain::tree_guides, iOS TreeGuides.swift, Android TreeGuides.kt), copied verbatim so a Section of Rows connects like a session tree or a PR stack. apply_tree_guides is the ONE post-expansion pass (the end of crate::macros::expand_macros): over every node’s children in order, each Row root contributes its numeric recipe.props.depth (anything else counts as 0 and ends every subtree), and the rule fills the Row’s guides part (a TreeGuides native) with elbowAt / tee / passThrough. Locked by fixtures/tree-guides.json and the Row cases of catalog-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. Mirrors packages/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. Mirrors src/validate.ts.
voice_wave
NEW-77: the VoiceWave rings (catalog/style.json voiceWave), a port of src/voice-wave.ts; fixtures/voice-wave.json locks 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 at child_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. Mirrors src/zoom.ts; fixtures/zoom.json locks it.