Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .agents/docs/binding-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The `calc` feature is enabled by Taffy's default features, which this repository

### Selected JavaScript container semantics

The direct analogue of Rust's update syntax is a plain `StyleInput` object whose fields are optional. A missing field or a field whose value is `undefined` takes the corresponding `Style::DEFAULT` value. Node creation stores the resulting complete Style. `setStyle(node, input)` constructs another complete Style from defaults and the supplied fields and replaces the old Style; it does not merge omitted fields with the node's current Style. A separate merge or patch API would be additive convenience with different semantics.
The direct analogue of default-based Rust Style construction is a plain `StyleInput` object whose fields are optional. A missing field or a field whose value is `undefined` takes the corresponding `Style::DEFAULT` value. Node creation stores the resulting complete Style. `setStyle(node, input)` constructs another complete Style from defaults and the supplied fields and replaces the old Style; it does not merge omitted fields with the node's current Style. The additive `updateStyle(node, update)` operation instead preserves omitted values.

Fixed-shape geometry records used directly as Style fields are partial on input for the same default-based construction. Each missing or explicit-`undefined` `Point`, `Size`, `Rect`, or `Line` component comes from that component of the enclosing field's `Style::DEFAULT` value. Thus `size: { width: value }` gets the default `auto` height, `padding: { left: value }` gets zero for the other three sides, and `inset: { left: value }` gets `auto` for the other three sides. This never reads the stored Style. Unknown enumerable string components are rejected because every component is optional and a misspelling would otherwise silently select the default. This rule does not make tagged semantic values, grid payloads, or arbitrary nested records recursively partial.

Expand Down Expand Up @@ -155,12 +155,20 @@ export interface Style {

The reusable geometry declarations referenced here are selected below.

The explicit `| undefined` members keep the declarations truthful for consumers that enable TypeScript's `exactOptionalPropertyTypes`: the runtime accepts both a missing property and an explicitly undefined property. Those forms use the field's default, while `null` explicitly requests `None` for a publicly nullable field. Conversion stores only the resulting Rust value: when a default is `Some(value)`, omission or `undefined` and `null` remain observably different; when the default is already `None`, they converge. Every property where `null` and `undefined` differ must explain both meanings in property-level JSDoc rather than relying only on an interface-level note. A complete `Style` keeps every field present and readonly, emits the concrete value for `Some(value)`, emits `null` for `None`, and never uses a missing or `undefined` field for nullable output.
The explicit `| undefined` members keep the declarations truthful for consumers that enable TypeScript's `exactOptionalPropertyTypes`: the runtime accepts both a missing property and an explicitly undefined property. Those forms use the field's default for construction and replacement and preserve the field for update, while `null` explicitly requests `None` for a publicly nullable field. Conversion stores only the resulting Rust value: when a default is `Some(value)`, omission or `undefined` and `null` remain observably different during replacement; when the default is already `None`, they converge. Every property where `null` and `undefined` differ must explain the operation-specific omission and explicit-null meanings in property-level JSDoc rather than relying only on an interface-level note. A complete `Style` keeps every field present and readonly, emits the concrete value for `Some(value)`, emits `null` for `None`, and never uses a missing or `undefined` field for nullable output.

`StyleInput` properties remain mutable in TypeScript. Conversion uses ordinary property access and does not inspect, reject, copy, freeze, or repeat validation solely because an input may contain accessors or be a Proxy; the caller owns those behaviors and side effects. Every binding-produced `Style` record uses readonly TypeScript properties because it is a detached snapshot whose mutation cannot change native state. The direct runtime representation is a complete eagerly materialized ordinary plain object without runtime freezing, sealing, or a Proxy. No lazy or selective output facility belongs to the initial API.

The first checkpoint is demonstrated by ordinary integration cases: `{}` and explicit `undefined` produce Taffy defaults; `setStyle(node, {})` resets a previously nondefault Style instead of merging; a partial fixed-shape nested record fills each omitted component from the corresponding enclosing Style default rather than stored state; an unknown top-level or partial-record component and `null` for a required field throw without changing the stored Style; `null` succeeds only for a publicly nullable field; a complete output uses `null` rather than an omitted or undefined field for Rust `None`; and declarations accept explicit `undefined` under `exactOptionalPropertyTypes` and retain the required property-level JSDoc. Getter- or Proxy-driven tree mutation is deliberately not a baseline fixture.

### Additive partial updates

`StyleUpdate` reuses the structural field types of `StyleInput`, including readonly collection inputs, without applying a recursive `Partial`. `updateStyle` interprets a missing or explicit-`undefined` outer field as “preserve,” and interprets missing components of the four public partial geometry families the same way. A supplied shorthand geometry value still supplies every component. Arrays, tagged-union branches, and other complete input records remain complete whole replacements; `[]` clears a collection and accepted `null` maps to `None`.

Rust converts the JavaScript object and all supplied collections into an owned patch before tree access. It compares only supplied fields and components with the stored Style, including bitwise comparison for direct floating-point fields so repeated `NaN` and distinct signed zero behave predictably. Empty and unchanged patches do not call Taffy's dirtying setter. For a real change, the complete candidate is validated before one `set_style` call, so a failure preserves both the old Style and dirty state.

Taffy 0.13 exposes the stored value through `style(&self) -> &Style` and replaces it through `set_style(Style)`, with no public mutable or take operation. `std::mem::take` would move a `Vec` allocation rather than clone it if the binding had `&mut Style`, but that reference cannot be obtained safely from the high-level tree. The implemented fallback returns an empty patch before cloning; every nonempty patch clones the current Style once, then compares and applies supplied fields in one traversal. An unchanged candidate is discarded without dirtying, while a changed candidate is validated and written once. Replacing a collection can therefore clone the old collection before overwriting it, but there is no workload benchmark justifying the larger selective-reconstruction implementation in the initial API. This preserves Taffy's sole ownership and avoids a JavaScript shadow Style; a future upstream closure-style update operation could safely expose in-place mutation and automatic dirty propagation.

The pointer-backed calc variant is a stop rather than a routine field mapping. The public high-level Style vocabulary excludes calc because `TaffyTree` resolves every calc pointer to zero and exposes no resolver. Whether the Cargo feature is also disabled is an implementation choice to verify separately; keeping an internal feature enabled does not make calc a supported JavaScript value.

### Selected closed-enum representation
Expand Down
8 changes: 6 additions & 2 deletions .agents/docs/binding-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ Ordinary input objects are read through normal JavaScript property access. Acces

`StyleInput` is a partial object used to construct one complete Rust `Style`. A missing field or explicit `undefined` uses `Style::DEFAULT`. `null` maps to `None` only for `aspectRatio`, the six nullable alignment fields, and `gridTemplateAreas`; other fields reject it. `setStyle` replaces the complete Style rather than merging with the old value.

The outer Style object rejects unknown enumerable string fields. Style geometry fields use partial named records; missing components use the matching component of the enclosing Style default, and unknown components are rejected. Complete inputs such as layout available space and a measure result require every component.
`StyleUpdate` has the same structural field types but different presence semantics. `updateStyle` preserves a missing or explicit-`undefined` field, and a partial `Point`, `Size`, `Rect`, or `Line` preserves each missing component. Supplied arrays, tagged unions, and other complete records replace their stored values as a whole; an empty array clears the collection, and accepted `null` still maps to `None`. This is not a recursive `Partial` operation.

Named geometry uses `x/y`, `width/height`, `left/right/top/bottom`, or `start/end`. Input records are mutable in TypeScript. Homogeneous semantic-length `Size` and `Rect` Style fields also accept one contained value and expand it to every component.
The outer Style object rejects unknown enumerable string fields. Style geometry fields use partial named records; missing components use the matching enclosing Style default during construction or replacement and preserve the matching stored component during update. Unknown components are rejected. Complete inputs such as layout available space and a measure result require every component.

Named geometry uses `x/y`, `width/height`, `left/right/top/bottom`, or `start/end`. Input records are mutable in TypeScript. Homogeneous semantic-length `Size` and `Rect` Style fields also accept one contained value and expand it to every component for both replacement and update.

Native update conversion produces an owned patch and component-presence masks before borrowing the tree. Under Taffy 0.13, `TaffyTree::style` exposes only `&Style` and `set_style` accepts a complete replacement; there is no public mutable, take, swap, or closure-based Style operation. The binding returns an empty patch before cloning. For a nonempty patch, it clones the stored Style once, applies and compares supplied values in the same traversal, discards an unchanged candidate without dirtying, and otherwise validates the complete candidate before calling `set_style` once. This deliberately favors one straightforward application inventory over an unbenchmarked selective-clone path; replacing a collection can therefore clone the old collection before overwriting it. Safe reuse of the current Style's allocations is not available through the pinned Taffy API. A future upstream closure-style mutation API that owns dirty propagation could remove the copy without changing the JavaScript contract.

### Numbers and closed families

Expand Down
12 changes: 12 additions & 0 deletions .agents/docs/taffyjs-node-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ New public state owners, compatibility layers, retained JavaScript values, callb

**Source:** Yunfei (`@hyfdev`), 2026-08-15; required examples and ordinary tests to prefer available shorthand, required JSDoc to name the corresponding complete form, accepted generator ownership with direct Rust normalization instead of JavaScript object materialization, and explicitly asked for the resulting design to be vouched. See [Generated tagged inputs](api-codegen.md#generated-tagged-inputs).

### Partial style updates

[VOUCHED @hyfdev 2026-08-16]

**Ruling:** `TaffyTree.updateStyle` must preserve every omitted or `undefined` value and update only the supplied parts of a node's current Style, while `setStyle` continues to replace the complete Style from Taffy's defaults. A public partial `Point`, `Size`, `Rect`, or `Line` input, such as `size`, `margin`, or `padding`, updates only its supplied components. Every array, tagged union, and other input modeled as a complete value is replaced as a whole; an empty array clears that array, and `null` clears only a field whose public input already permits null. The public TypeScript update type must keep arrays, every tagged-union branch, and complete records complete; it must not use a general recursive `Partial`.

**Limits:** `updateStyle` does not update an array element or a tagged-union payload in place. A concrete future need may add a separate array-editing operation without changing these rules. This decision does not fix the private field numbering, transport shape, parsing strategy, generated type name, or whether a later Taffy API can avoid copying a complete Rust Style.

**Why:** Callers should be able to change an independently meaningful style part without reconstructing unrelated Style data, while values whose meaning depends on their complete variant or sequence remain predictable. Copying, combining, and validating the current and supplied values belongs in Rust rather than in a `getStyle()` to JavaScript merge to `setStyle()` round trip. The complete prospective Style must be validated before one write; an invalid update changes nothing, and an empty update or an update whose result is unchanged must leave the node's existing dirty state unchanged rather than making a clean node dirty. These rules keep the common API friendly without introducing recursive array merging, partial tagged variants, or a JavaScript-owned shadow Style.

**Source:** Yunfei (`@hyfdev`), 2026-08-16; kept `setStyle` as the direct complete-replacement operation, chose an additive update operation for ergonomic partial changes, required Rust-owned copying rather than JavaScript object cloning, required invalid, empty, and unchanged updates not to produce mutation or new dirty state, accepted whole-array replacement, and explicitly confirmed that generated TypeScript must prevent partial tagged-union inputs before asking for this accumulated design to be vouched.

## Open

### Selective query implementation details
Expand Down
20 changes: 19 additions & 1 deletion apps/website/guide/styles-and-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ style.flexGrow === 0; // true: Taffy's default

Keep a shared base object in your own code when several replacements should preserve the same fields.

## `updateStyle` preserves omitted values

Use `updateStyle` when only part of the current style should change. Omitted fields and explicit `undefined` preserve their stored values. The same rule applies to omitted components of the partial `Point`, `Size`, `Rect`, and `Line` records:

```ts
tree.updateStyle(node, {
flexGrow: 3,
size: { width: 320 },
margin: { left: 20 },
});
```

Here the current height and the other three margins remain unchanged. A single shorthand value still supplies every component, so `padding: 12` replaces all four padding sides.

Arrays, tagged values, and other complete records are replaced as complete values rather than merged recursively. For example, `gridAutoRows: []` clears all automatic row tracks, and `flexBasis: Dimension.Auto` replaces the complete tagged basis value. `null` clears only a field whose public type permits it.

The prospective merged style is validated before it is stored. A failed update changes neither style nor dirty state. An empty update or one that supplies only already-stored values also leaves dirty state unchanged.

## Named constants and tagged values

Closed choices use frozen numeric families such as `Display`, `Overflow`, `FlexDirection`, and `AlignItems`:
Expand Down Expand Up @@ -78,4 +96,4 @@ const reusableInput: StyleInput = snapshot;
tree.setStyle(otherNode, reusableInput);
```

Changing `snapshot` would not update `node`; call `setStyle` to make a real change. The generated declarations remain the exhaustive field reference. The Guide pages focus on the groups of fields that work together.
Changing `snapshot` would not update `node`; call `setStyle` or `updateStyle` to make a real change. The generated declarations remain the exhaustive field reference. The Guide pages focus on the groups of fields that work together.
4 changes: 2 additions & 2 deletions apps/website/node/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ try {

## JavaScript error classes

`TypeError` reports the wrong JavaScript type or object shape, such as a non-bigint node ID, an incomplete available-space record, an unknown `StyleInput` field, or a non-function measure callback.
`TypeError` reports the wrong JavaScript type or object shape, such as a non-bigint node ID, an incomplete available-space record, an unknown style input or update field, or a non-function measure callback.

`RangeError` reports a numeric value that cannot represent the requested public value, such as an unknown numeric-family code, a fractional child index, an out-of-range Grid line, or an invalid child range. The out-of-bounds child-index case also carries the code shown above.

Other Taffy operation failures use `Error`. Check a documented code when the program can recover from that specific condition; do not parse message text.

## Failed mutations

Inputs are converted and validated before a mutation reaches the tree. A rejected node creation does not consume a public node ID or add a node. A rejected style replacement leaves the old style and dirty state unchanged. A rejected topology operation leaves the previous parents, child order, contexts, and node count unchanged.
Inputs are converted and validated before a mutation reaches the tree. A rejected node creation does not consume a public node ID or add a node. A rejected style replacement or partial update leaves the old style and dirty state unchanged. A rejected topology operation leaves the previous parents, child order, contexts, and node count unchanged.

Invalid node IDs also fail before the requested operation changes either the JavaScript wrapper or native tree.

Expand Down
4 changes: 2 additions & 2 deletions apps/website/node/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ The package does not parse CSS, create DOM elements, or render output. It also d
## API

- [Nodes and Topology](./nodes-and-topology.md) covers node creation, inspection, parent-child changes, removal, `clear`, and `NodeId` lifetime.
- [Styles and Context](./styles-and-context.md) covers style replacement, style snapshots, JavaScript context, and measurement invalidation.
- [Styles and Context](./styles-and-context.md) covers style replacement and partial updates, style snapshots, JavaScript context, and measurement invalidation.
- [Computing Layout](./computing-layout.md) covers available space, dirty state, rounding, measurement, caching, and callback restrictions.
- [Layout Results](./layout-results.md) covers ordinary, unrounded, and detailed Grid output.
- [Style](./style.md) groups the fields accepted by `StyleInput` and returned by `Style`.
- [Style](./style.md) groups the fields accepted by `StyleInput` and `StyleUpdate` and returned by `Style`.
- [Value Helpers](./value-helpers.md) covers numeric constants and tagged values used throughout style and computation inputs.
- [Errors](./errors.md) covers stable error codes, JavaScript error classes, and state after failure.
Loading