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
14 changes: 14 additions & 0 deletions .agents/docs/binding-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ Rust converts the JavaScript object and all supplied collections into an owned p

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.

A focused end-to-end native-binding microbenchmark on implementation commit `6ed48b4` retained the JavaScript-to-Rust boundary, input conversion, mutation, and alternating real value changes. It ran on Node.js 24.19.0 on Linux with an Intel Core i5-13500H, pinned to one core, after warmup, with 13 samples in each of two runs. Representative per-operation timings were:

| Preserved state and changed value | `setStyle` reconstruction | `updateStyle` | Observed relation |
| ------------------------------------------------------------ | ------------------------: | ------------: | ------------------------: |
| One retained scalar; replace one scalar | 3.83 µs | 3.89 µs | Effectively equal |
| 13 retained sparse fields; replace one scalar | 11.8 µs | 3.89 µs | `updateStyle` 3.0× faster |
| Complete 41-field `getStyle` snapshot; replace one scalar | 23.1 µs | 3.97 µs | `updateStyle` 5.8× faster |
| 1,000 retained `gridAutoRows`; replace one scalar | 428 µs | 4.60 µs | `updateStyle` 93× faster |
| Replace 1,000 `gridAutoRows` | 446 µs | 448 µs | Effectively equal |
| 1,000 retained nested string grid values; replace one scalar | 139 µs | 64.3 µs | `updateStyle` 2.2× faster |
| Replace 1,000 nested string grid values | 139 µs | 196 µs | `setStyle` 1.4× faster |

These measurements support recommending `updateStyle` for incremental changes: it avoids reconverting retained JavaScript fields, and the advantage grows when retained input is larger. They do not establish per-call dominance. When the changed field itself is the large collection, both operations must parse its replacement and can converge; with string-heavy replacements, the current full-Style clone fallback can make `updateStyle` slower. This focused boundary microbenchmark also does not justify adding selective reconstruction without representative workload evidence. Public documentation should therefore say that `updateStyle` is generally faster for incremental changes, not that every call must be faster.

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
10 changes: 10 additions & 0 deletions .agents/docs/taffyjs-node-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ New public state owners, compatibility layers, retained JavaScript values, callb

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

### Preferred style mutation operation

**Ruling:** Public documentation must recommend `updateStyle` as the default operation for changing an existing node. `setStyle` is the intentional complete-replacement operation and should be chosen when omitted fields must reset to Taffy's defaults, including a complete reset with `{}`.

**Limits:** This recommendation does not remove or deprecate `setStyle`, change whole-value replacement for supplied arrays and complete values, promise that every individual `updateStyle` call is faster, or fix the binding's private copying strategy. The exact cost depends on the input shape and runtime.

**Why:** Reconstructing preserved state for `setStyle` requires a caller-owned prior value or a `getStyle` snapshot and makes every retained supplied value cross the JavaScript-to-Rust conversion boundary again. `updateStyle` directly represents the incremental intent and generally avoids converting unrelated JavaScript values. Retained end-to-end measurements support this qualitative default while also showing that direct replacement of a large collection can make the operations converge, so the public claim must remain comparative rather than absolute.

**Source:** Yunfei (`@hyfdev`), 2026-08-16; explicitly requested a dedicated `@taffyjs/node` documentation page that explains the distinction and tells readers to prefer `updateStyle` unless they have a specific need for `setStyle`.

## Open

### Selective query implementation details
Expand Down
2 changes: 1 addition & 1 deletion .agents/docs/website.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The target page groups are:

- Guide: Introduction and Getting Started only.
- Essentials: the tree-compute-read workflow, styles and values, Block, Flexbox, Grid, and measuring text and images.
- `@taffyjs/node`: package overview and supported targets; `TaffyTree` node and topology operations, styles and context, computation and dirty state, and layout results; the grouped `Style` data model; value helpers; and errors.
- `@taffyjs/node`: package overview and supported targets; `TaffyTree` node and topology operations, styles and context, the detailed `setStyle` versus `updateStyle` choice, computation and dirty state, and layout results; the grouped `Style` data model; value helpers; and errors.
- `@taffyjs/wasm`: package overview and setup, supported environments, initialization and deployment, observable differences from Node, and a link to the shared direct API reference.
- `@taffyjs/yoga`: package overview and installation, compatibility scope, migration from Yoga, Yoga-facing API reference, and documented differences from the direct Taffy API.

Expand Down
4 changes: 4 additions & 0 deletions apps/website/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const documentationSidebar = [
{ text: "Overview", link: "/node/" },
{ text: "Nodes and Topology", link: "/node/nodes-and-topology" },
{ text: "Styles and Context", link: "/node/styles-and-context" },
{
text: "setStyle vs updateStyle",
link: "/node/set-style-vs-update-style",
},
{ text: "Computing Layout", link: "/node/computing-layout" },
{ text: "Layout Results", link: "/node/layout-results" },
{ text: "Style", link: "/node/style" },
Expand Down
2 changes: 2 additions & 0 deletions apps/website/guide/styles-and-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Arrays, tagged values, and other complete records are replaced as complete value

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.

Unless you intentionally want a complete replacement, prefer `updateStyle` when changing an existing node. [setStyle vs updateStyle](../node/set-style-vs-update-style.md) explains the semantic and performance tradeoff in detail.

## Named constants and tagged values

Closed choices use frozen numeric families such as `Display`, `Overflow`, `FlexDirection`, and `AlignItems`:
Expand Down
89 changes: 89 additions & 0 deletions apps/website/node/set-style-vs-update-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# setStyle vs updateStyle

When changing an existing node, prefer `updateStyle`. Use `setStyle` only when you intentionally want to replace the node's complete style or reset omitted fields to Taffy's defaults.

Both methods accept ordinary partial-looking JavaScript objects, but omission has a different meaning:

| Operation | `setStyle(node, input)` | `updateStyle(node, update)` |
| ------------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------- |
| Omitted top-level field | Reset to its Taffy default | Preserve the stored value |
| Explicit `undefined` | Reset to its Taffy default | Preserve the stored value |
| Omitted component in `size`, `margin`, or another partial geometry record | Reset that component to its default | Preserve that component |
| Supplied array, tagged value, or complete record | Replace the complete value | Replace the complete value |
| Empty object | Reset the complete style | Make no change |
| Supplied values already match the stored values | Perform a complete replacement | Make no change and do not newly dirty the node |

## Choose by intent

Suppose a node already has a display mode, size, and flex growth:

```ts
const node = tree.newLeaf({
display: Display.Grid,
size: { width: 320, height: 180 },
flexGrow: 1,
});
```

`updateStyle` changes only what you name:

```ts
tree.updateStyle(node, { flexGrow: 2 });

// display and size are preserved
```

The same object passed to `setStyle` means something different:

```ts
tree.setStyle(node, { flexGrow: 2 });

// display and size are reset to their Taffy defaults
```

This makes `updateStyle` the direct expression of an incremental change. It also avoids keeping a complete JavaScript shadow of native style state merely so that unrelated values can be supplied again.

## Why `updateStyle` is generally faster

A `setStyle` replacement must convert every supplied field from JavaScript into its native Taffy representation. Rebuilding the replacement with object spread does not avoid that work:

```ts
const nextStyle = { ...previousStyle, flexGrow: 2 };
tree.setStyle(node, nextStyle);
```

The spread is shallow, but all retained fields in `nextStyle` still cross the JavaScript-to-native boundary again. Arrays and nested style values must be visited and converted even when they did not change. If `previousStyle` came from `getStyle`, producing that complete snapshot adds another native-to-JavaScript conversion first.

`updateStyle` sends and converts only the supplied fields, then combines them with the stored style on the native side:

```ts
tree.updateStyle(node, { flexGrow: 2 });
```

The advantage usually grows with the number and complexity of retained fields. Small scalar-only styles may be close, while preserving large arrays or nested values can make reconstructing a `setStyle` input substantially more expensive.

This is a default choice, not a promise that every individual `updateStyle` call is faster. Replacing a large collection still requires converting that collection, and exact costs depend on the values and runtime. The general rule remains: unless complete replacement is the intended behavior, prefer `updateStyle`.

## When `setStyle` is the right operation

Use `setStyle` when the input is the authoritative complete replacement and omission should mean “use the default.” Common examples are:

- Resetting every field with `tree.setStyle(node, {})`.
- Applying a complete style snapshot or configuration that should replace earlier state.
- Deliberately clearing any old fields that the replacement does not name.

Do not use `setStyle` plus `getStyle` or a caller-owned shadow object merely to update one field. That recreates incremental-update behavior in JavaScript and pays for converting the retained data again.

## Updates are shallow by design

`updateStyle` preserves omitted top-level fields and omitted components of partial `Point`, `Size`, `Rect`, and `Line` records. It does not recursively merge every nested value. A supplied array replaces the whole array, and a supplied tagged value or complete record replaces that whole value:

```ts
tree.updateStyle(node, {
size: { width: 480 }, // preserves the current height
gridAutoRows: [], // clears every automatic row track
flexBasis: Dimension.Auto, // replaces the complete tagged value
});
```

See [Styles and Values](../guide/styles-and-values.md) for the shared value rules and the [Style reference](./style.md) for the complete field groups.
2 changes: 2 additions & 0 deletions apps/website/node/styles-and-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Floating-point style values accept JavaScript numbers and are stored with Taffy'

`getStyle(node)` returns a complete detached snapshot, including defaults. Its fields are recursively readonly in TypeScript, but its runtime objects are not frozen. Mutating a returned object does not change the tree. A style snapshot can be passed back as a later `StyleInput` or `StyleUpdate` because its structure is compatible.

For ordinary changes to an existing node, prefer `updateStyle`. Use `setStyle` when complete replacement or resetting omitted fields to their defaults is the intended operation. See [setStyle vs updateStyle](./set-style-vs-update-style.md) for the detailed comparison and performance model.

## Store JavaScript context

The generic parameter on `TaffyTree<TContext>` describes the values returned by `getNodeContext` and supplied to measurement:
Expand Down