Custom elements

Prefer a functional JSX component when existing elements can express the layout. Extend Element when you need a new primitive or layout policy. The inherited constructor handles the name, defaults, source ownership, and layout descriptor. An ordinary primitive needs only a static layout method.

Element is also available directly inside evaluated JSX: omit the imports and TypeScript annotations, define the class, and use <Tile />. A class defined in host code can be supplied through evaluate(code, { scope: { Tile } }).

Scoped props

prefix_split(prefixes, props, keep?) separates component scopes without mutating the input. It returns one object per prefix, in the requested order, then the remaining props. The longest matching prefix wins when prefixes overlap. Optional exact keep keys remain with the owner. prefix_join(prefix, props) adds the prefix to every key. Neither helper resolves units or executes callbacks.

Both helpers are available in JSX and as named exports from gum-jsx-core:

Prefixed<Prefix, Props> derives scoped TypeScript names while retaining each property's value type and optionality:

Import BoxProps, Prefixed, and TextOptions as types from gum-jsx-core. TextOptions is TextProps without text/children, which the owner supplies. Components choose which scopes to support and where to route them; LayoutPass has no prefix policy. See Style for built-in scopes and precedence.

Scopes are constructor-input syntax. Class defaults below still use normalized source props: Arrow defaults may specify head_style, while head_fill is an input alias. Defaults that affect child construction belong in a component or normalizer before the child descriptions are built.

Class hooks and defaults

Static memberRole
defaultsOptional source props, merged over inherited defaults
layout(props, query)Required behavior; returns a fragment during layout
normalize(input)Optional construction-time conversion to source props
data_bounds(props)Optional graph bounds without measurement
element_nameOptional explicit diagnostic name; otherwise the subclass name is used

Defaults and hook references are captured on a class's first use, including use as an ancestor. Set them before constructing instances. Defaults are deeply copied; the caller's original records stay mutable. Each instance overrides the merged defaults, including explicit undefined values. Nested records are replaced rather than deep-merged.

In TypeScript, annotate overridable defaults as Partial<Props> so descendants can supply only the properties they change. A child inherits layout and optional hooks; an override replaces that hook. Hooks receive the concrete class as this, including when inherited. An explicit element_name belongs to the class that declares it; descendants normally receive their own class names. Bundles that rename classes should preserve class names or supply explicit names.

The layout method runs during layout, not construction. Source props are immutable snapshots: plain records, arrays, primitive values, and Element references. Stored props cannot contain functions, font objects, host handles, or caches. Behavior belongs to the element type and resources belong to LayoutPass. Keep instance state in props or the layout pass. When runtime immutability checks are enabled, the base constructor freezes the instance, and ordinary field initializers or assignments after super() throw. The readonly contract also applies with checks disabled; see immutability and performance.

Normalization and components

normalize(input) runs once on raw constructor input, before source defaults are merged into its result. It can consume sampling or styling callbacks and expand child descriptions. Give the normalizer its own input fallbacks; source defaults are not passed into it. This order matches define_element.

When the input differs from stored data, use Element<SourceProps, InputProps>:

Here PointValue and point_bounds are additional exports from gum-jsx-core. data_bounds(props) reports graphable data limits without measurement; see Coordinates.

define_element(name, layout, defaults?, options?) remains available and uses the same base-class machinery. Its defaults are captured when the factory is called; options accepts normalize and data_bounds:

define_component(name, build) adopts an existing element's description and protocol, with a new name and no extra layout wrapper. It preserves the static layout and bounds behavior without retaining the original subclass or rerunning its normalizer. The low-level new Element(type, props) constructor remains available for explicit protocol adoption. Adopted Page and Slide components keep their viewport behavior: Document accepts them as pages, applies its defaults directly, and rendering does not add another Page wrapper.

The query contract

Query memberRole
requestPrepared natural, available, or exact requests for both axes
sizingResolved source sizes, bounds, and aspect preference
styleResolved inherited style, including pixel font size
measureShared length context: local font size, parent reference, and diagnostic path
coordinatesOptional ambient data limits and flip directions
mathOptional ambient math layout context
child(element, request, reference?, index?, context?)Measure a child; context can override style, coordinates, and math
resource(name)Obtain a host resource from this pass
prepare(name, compute)Cache preparation independent of requests and parent references

Pass query.measure directly to length helpers. For example, resolve_length(props.gap, query.measure, size.width, 'gap') resolves em from that context, uses size.width for a numeric fraction, and adds .gap to the diagnostic path. The fraction reference is explicit because each property chooses its own axis or geometry. resolve_insets(props.padding, query.measure) chooses axes from query.measure.reference for each side.

The context is an immutable LengthContext. Use make_measure(query.measure, { font_size }) to derive a different local font while retaining its parent reference and path. Likewise, a patch can replace reference or path. A container that resolves a child's placement or sizing props before measuring it can use child_measure(child, query, index, reference) to derive the child's font and path consistently.

Return a fragment in local pixel coordinates. For a shape-like leaf, shape_size resolves its size. For a content-sized element, measure children and pass the desired size through finish_size before constructing the result. An inconsistent returned size is an error; a renderer cannot repair it later. Drawing helpers also take final pixel values, including stroke widths.

The common aspect property is already applied to query.request when one axis is established, so custom elements receive the derived allocation before layout. With both axes content-sized, finish_size adds allocation space to reach the preferred ratio. Do not implement separate aspect inference or scale the content.

A container must establish its own percentage references where dimensions are definite, query children through query.child, and place the returned fragments with place_fragment. The child's default reference argument is empty, not the container's inherited reference. Parent placement does not mutate the child or remeasure it. Validate your own props and child policy explicitly.

Prepared values may depend on source, style, math context, and versioned resources. Current requests, parent percentage references, and coordinates belong in ordinary layout work. Set a new resource version on a reused pass when external data changes; see Fonts for an example.

The runnable Meter defines a small custom leaf and a CompactMeter subclass using only evaluator bindings. Its normalizer validates and clamps the input once. Its drawings use the size actually allocated to it, and its value is explicit source data. It does not add an implicit growth policy to stacks.