diff --git a/.agents/docs/binding-cases.md b/.agents/docs/binding-cases.md index 3901861..440dafc 100644 --- a/.agents/docs/binding-cases.md +++ b/.agents/docs/binding-cases.md @@ -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. @@ -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 diff --git a/.agents/docs/binding-mapping.md b/.agents/docs/binding-mapping.md index 18d68dd..a66c82a 100644 --- a/.agents/docs/binding-mapping.md +++ b/.agents/docs/binding-mapping.md @@ -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 diff --git a/.agents/docs/taffyjs-node-decisions.md b/.agents/docs/taffyjs-node-decisions.md index 4b696c5..c4729d4 100644 --- a/.agents/docs/taffyjs-node-decisions.md +++ b/.agents/docs/taffyjs-node-decisions.md @@ -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 diff --git a/apps/website/guide/styles-and-values.md b/apps/website/guide/styles-and-values.md index 9dfac29..8ce679d 100644 --- a/apps/website/guide/styles-and-values.md +++ b/apps/website/guide/styles-and-values.md @@ -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`: @@ -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. diff --git a/apps/website/node/errors.md b/apps/website/node/errors.md index cb102a1..9f1c381 100644 --- a/apps/website/node/errors.md +++ b/apps/website/node/errors.md @@ -29,7 +29,7 @@ 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. @@ -37,7 +37,7 @@ Other Taffy operation failures use `Error`. Check a documented code when the pro ## 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. diff --git a/apps/website/node/index.md b/apps/website/node/index.md index 4770615..d5737df 100644 --- a/apps/website/node/index.md +++ b/apps/website/node/index.md @@ -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. diff --git a/apps/website/node/style.md b/apps/website/node/style.md index 198928b..6af0a7f 100644 --- a/apps/website/node/style.md +++ b/apps/website/node/style.md @@ -1,6 +1,6 @@ # Style -`StyleInput` is the partial object accepted when a node is created or passed to `setStyle`. Omitted fields and explicit `undefined` use Taffy's defaults. `Style` is the complete detached object returned by `getStyle` and supplied to measure callbacks. +`StyleInput` is the partial object accepted when a node is created or passed to `setStyle`; omitted fields and explicit `undefined` use Taffy's defaults. `StyleUpdate` has the same accepted field shapes for `updateStyle`, but omitted fields and explicit `undefined` preserve stored values. `Style` is the complete detached object returned by `getStyle` and supplied to measure callbacks. Concrete lengths can usually be written as numbers. For example, `size: { width: 200 }` is the concise form of `size: { width: Dimension.Length(200) }`. Percentage, automatic, intrinsic, and Grid-specific values remain explicit. [Styles and Values](../guide/styles-and-values.md) explains these input forms in context. @@ -17,7 +17,7 @@ These fields describe the node itself or are shared by more than one layout mode | `margin`, `padding`, `border`, `gap` | Supply spacing around, inside, and between boxes. | | `alignItems`, `alignSelf`, `justifyItems`, `justifySelf`, `alignContent`, `justifyContent` | Align items, individual nodes, or groups of lines and tracks. | -Geometry fields accept either one supported value for every component or a partial named record. For example, `padding: 12` applies to all four sides, while `padding: { left: 12, right: 12 }` changes only those sides from their defaults. +Geometry fields accept either one supported value for every component or a partial named record. For example, `padding: 12` applies to all four sides. With creation or `setStyle`, `padding: { left: 12, right: 12 }` fills the other sides from defaults; with `updateStyle`, it preserves the other stored sides. The optional fields `aspectRatio`, the six alignment fields, and `gridTemplateAreas` accept `null` to store Taffy's absent value. Other fields reject `null`. @@ -56,8 +56,8 @@ The shared alignment fields control placement along the main and cross axes. See The [Grid guide](../guide/grid.md) introduces the helper values used by these fields and shows a complete computation. -## Reading and replacing style +## Reading, replacing, and updating style -`getStyle(node)` returns every field, including defaults, as a detached `Style` snapshot. That snapshot can be passed back anywhere a `StyleInput` is accepted. `setStyle(node, input)` replaces the complete stored style rather than merging with the previous one; see [Styles and Context](./styles-and-context.md) for the exact update and invalidation behavior. +`getStyle(node)` returns every field, including defaults, as a detached `Style` snapshot. That snapshot can be passed back anywhere a `StyleInput` or `StyleUpdate` is accepted. `setStyle(node, input)` replaces the complete stored style from defaults. `updateStyle(node, update)` preserves omitted fields and omitted partial-geometry components while replacing arrays, tagged values, and complete records as whole values. See [Styles and Context](./styles-and-context.md) for the exact mutation and invalidation behavior. The package declarations and JSDoc remain the exact reference for each field's accepted TypeScript shape and each numeric constant member. This page groups the fields by their role so related choices can be found together. diff --git a/apps/website/node/styles-and-context.md b/apps/website/node/styles-and-context.md index de8eb6d..e92c19d 100644 --- a/apps/website/node/styles-and-context.md +++ b/apps/website/node/styles-and-context.md @@ -2,9 +2,9 @@ Styles are native layout data. Context is arbitrary JavaScript data associated with a node. They have separate owners and update rules even though both can affect measurement. -The [Style reference](./style.md) groups the fields accepted by `StyleInput` and returned by `Style`. This page covers the `TaffyTree` methods that replace and read those values, then the separate JavaScript context methods. +The [Style reference](./style.md) groups the fields accepted by `StyleInput` and `StyleUpdate` and returned by `Style`. This page covers the `TaffyTree` methods that replace, update, and read those values, then the separate JavaScript context methods. -## Replace and read styles +## Replace, update, and read styles `setStyle(node, style)` replaces the node's complete stored style. It does not merge with the previous value. Missing properties and explicit `undefined` are expanded from Taffy's defaults on every call: @@ -17,11 +17,23 @@ current.display === Display.Flex; // true current.flexGrow === 0; // true ``` -`null` is accepted only for the optional fields listed in the [Style reference](./style.md#shared-fields). Other fields reject it. Unknown top-level style fields and unknown components in partial geometry records are also rejected. A failed conversion leaves both the previous style and its dirty state unchanged. +`updateStyle(node, update)` instead preserves omitted fields and explicit `undefined`. Partial geometry records preserve their omitted components: + +```ts +tree.updateStyle(node, { + flexGrow: 2, + size: { width: 240 }, + margin: { left: 16 }, +}); +``` + +Arrays, tagged values, and other complete records are whole replacements. An empty array clears the stored array; `null` clears only a publicly nullable field. `updateStyle` does not recursively merge an array element or tagged-union payload. + +`null` is accepted only for the optional fields listed in the [Style reference](./style.md#shared-fields). Other fields reject it. Unknown top-level style fields and unknown components in partial geometry records are also rejected. Replacement and update conversion finish before mutation, and a failed operation leaves both the previous style and its dirty state unchanged. An empty or unchanged `updateStyle` call does not newly dirty a clean node; a successful changed update uses Taffy's normal dirty propagation. Floating-point style values accept JavaScript numbers and are stored with Taffy's 32-bit precision. They are not coerced from strings or objects, clamped, or replaced with binding-specific defaults; negative and non-finite numbers reach Taffy as numeric values. Integer codes, indices, spans, and counts instead must be finite integers in the range of the corresponding public value. -`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` because its structure is compatible. +`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. ## Store JavaScript context diff --git a/crates/taffyjs_binding/src/geometry.rs b/crates/taffyjs_binding/src/geometry.rs index 22985a3..6000816 100644 --- a/crates/taffyjs_binding/src/geometry.rs +++ b/crates/taffyjs_binding/src/geometry.rs @@ -46,12 +46,17 @@ pub(crate) fn partial_point( value: Unknown<'_>, default: Point, mut convert: impl FnMut(f64) -> BindingResult, -) -> BindingResult> { +) -> BindingResult<(Point, Point)> { let input: PartialPointInput = js_object::input(value, "a Point object", Some(POINT_FIELDS))?; - Ok(Point { + let present = Point { + x: input.x.is_some(), + y: input.y.is_some(), + }; + let value = Point { x: input.x.map(&mut convert).transpose()?.unwrap_or(default.x), y: input.y.map(&mut convert).transpose()?.unwrap_or(default.y), - }) + }; + Ok((value, present)) } pub(crate) fn size<'env, T>( @@ -70,10 +75,14 @@ pub(crate) fn partial_size<'env, T>( value: Unknown<'env>, default: Size, mut convert: impl FnMut(Unknown<'env>) -> BindingResult, -) -> BindingResult> { +) -> BindingResult<(Size, Size)> { let input: PartialSizeInput<'env> = js_object::input(value, "a Size object", Some(SIZE_FIELDS))?; - Ok(Size { + let present = Size { + width: input.width.is_some(), + height: input.height.is_some(), + }; + let value = Size { width: input .width .map(&mut convert) @@ -84,17 +93,24 @@ pub(crate) fn partial_size<'env, T>( .map(&mut convert) .transpose()? .unwrap_or(default.height), - }) + }; + Ok((value, present)) } pub(crate) fn partial_rect<'env, T>( value: Unknown<'env>, default: Rect, mut convert: impl FnMut(Unknown<'env>) -> BindingResult, -) -> BindingResult> { +) -> BindingResult<(Rect, Rect)> { let input: PartialRectInput<'env> = js_object::input(value, "a Rect object", Some(RECT_FIELDS))?; - Ok(Rect { + let present = Rect { + left: input.left.is_some(), + right: input.right.is_some(), + top: input.top.is_some(), + bottom: input.bottom.is_some(), + }; + let value = Rect { left: input .left .map(&mut convert) @@ -115,17 +131,22 @@ pub(crate) fn partial_rect<'env, T>( .map(&mut convert) .transpose()? .unwrap_or(default.bottom), - }) + }; + Ok((value, present)) } pub(crate) fn partial_line<'env, T>( value: Unknown<'env>, default: Line, mut convert: impl FnMut(Unknown<'env>) -> BindingResult, -) -> BindingResult> { +) -> BindingResult<(Line, Line)> { let input: PartialLineInput<'env> = js_object::input(value, "a Line object", Some(LINE_FIELDS))?; - Ok(Line { + let present = Line { + start: input.start.is_some(), + end: input.end.is_some(), + }; + let value = Line { start: input .start .map(&mut convert) @@ -136,5 +157,6 @@ pub(crate) fn partial_line<'env, T>( .map(&mut convert) .transpose()? .unwrap_or(default.end), - }) + }; + Ok((value, present)) } diff --git a/crates/taffyjs_binding/src/lib.rs b/crates/taffyjs_binding/src/lib.rs index 4a3928e..0552cba 100644 --- a/crates/taffyjs_binding/src/lib.rs +++ b/crates/taffyjs_binding/src/lib.rs @@ -446,6 +446,22 @@ impl BindingTaffyTree { ) } + #[napi(js_name = "rawUpdateStyle")] + pub fn update_style(&self, env: Env, node: BigInt, update: Unknown<'_>) -> napi::Result<()> { + let node = into_napi(env, raw_node_id(&node))?; + let update = into_napi(env, style::patch(update))?; + into_napi( + env, + self.owner.access("updateStyle", |tree| { + let current = tree.style(node).map_err(|_| internal_error())?; + let Some(updated) = style::apply_patch(current, update)? else { + return Ok(()); + }; + tree.set_style(node, updated).map_err(|_| internal_error()) + }), + ) + } + #[napi(js_name = "rawSetNodeContext")] pub fn set_node_context(&self, env: Env, node: BigInt, has_context: bool) -> napi::Result<()> { let node = into_napi(env, raw_node_id(&node))?; diff --git a/crates/taffyjs_binding/src/style.rs b/crates/taffyjs_binding/src/style.rs index 5fb20a2..4537aaa 100644 --- a/crates/taffyjs_binding/src/style.rs +++ b/crates/taffyjs_binding/src/style.rs @@ -1,7 +1,7 @@ use napi::ValueType; use napi::bindgen_prelude::{Either, Null, Unknown}; use napi_derive::napi; -use taffy::geometry::{Rect, Size}; +use taffy::geometry::{Line, Point, Rect, Size}; use taffy::style::{ AlignContent, AlignItems, BoxSizing, Clear, Direction, Display, FlexDirection, FlexWrap, Float, GridAutoFlow, Overflow, Position, Style, TextAlign, @@ -106,6 +106,56 @@ pub struct StyleInput<'env> { pub grid_column: Option>, } +#[derive(Default, PartialEq)] +struct StylePresence { + display: bool, + item_is_table: bool, + item_is_replaced: bool, + box_sizing: bool, + direction: bool, + overflow: Option>, + scrollbar_width: bool, + r#float: bool, + clear: bool, + position: bool, + inset: Option>, + size: Option>, + min_size: Option>, + max_size: Option>, + aspect_ratio: bool, + margin: Option>, + padding: Option>, + border: Option>, + align_items: bool, + align_self: bool, + justify_items: bool, + justify_self: bool, + align_content: bool, + justify_content: bool, + gap: Option>, + text_align: bool, + flex_direction: bool, + flex_wrap: bool, + flex_basis: bool, + flex_grow: bool, + flex_shrink: bool, + grid_template_rows: bool, + grid_template_columns: bool, + grid_auto_rows: bool, + grid_auto_columns: bool, + grid_auto_flow: bool, + grid_template_areas: bool, + grid_template_column_names: bool, + grid_template_row_names: bool, + grid_row: Option>, + grid_column: Option>, +} + +pub(crate) struct StylePatch { + value: Style, + presence: StylePresence, +} + #[napi(object, object_to_js = false)] pub struct MaybeTaggedLengthInput { pub unit: Option, @@ -208,13 +258,19 @@ fn is_length_input(value: Unknown<'_>) -> BindingResult { fn dimension_size( value: Unknown<'_>, default: Size, -) -> BindingResult> { +) -> BindingResult<(Size, Size)> { if is_length_input(value)? { let value = length::dimension(value)?; - Ok(Size { - width: value, - height: value, - }) + Ok(( + Size { + width: value, + height: value, + }, + Size { + width: true, + height: true, + }, + )) } else { geometry::partial_size(value, default, length::dimension) } @@ -223,15 +279,23 @@ fn dimension_size( fn auto_rect( value: Unknown<'_>, default: Rect, -) -> BindingResult> { +) -> BindingResult<(Rect, Rect)> { if is_length_input(value)? { let value = length::length_percentage_auto(value)?; - Ok(Rect { - left: value, - right: value, - top: value, - bottom: value, - }) + Ok(( + Rect { + left: value, + right: value, + top: value, + bottom: value, + }, + Rect { + left: true, + right: true, + top: true, + bottom: true, + }, + )) } else { geometry::partial_rect(value, default, length::length_percentage_auto) } @@ -240,15 +304,23 @@ fn auto_rect( fn length_rect( value: Unknown<'_>, default: Rect, -) -> BindingResult> { +) -> BindingResult<(Rect, Rect)> { if is_length_input(value)? { let value = length::length_percentage(value)?; - Ok(Rect { - left: value, - right: value, - top: value, - bottom: value, - }) + Ok(( + Rect { + left: value, + right: value, + top: value, + bottom: value, + }, + Rect { + left: true, + right: true, + top: true, + bottom: true, + }, + )) } else { geometry::partial_rect(value, default, length::length_percentage) } @@ -257,13 +329,19 @@ fn length_rect( fn length_size( value: Unknown<'_>, default: Size, -) -> BindingResult> { +) -> BindingResult<(Size, Size)> { if is_length_input(value)? { let value = length::length_percentage(value)?; - Ok(Size { - width: value, - height: value, - }) + Ok(( + Size { + width: value, + height: value, + }, + Size { + width: true, + height: true, + }, + )) } else { geometry::partial_size(value, default, length::length_percentage) } @@ -401,170 +479,484 @@ fn grid_auto_flow(value: f64) -> BindingResult { }) } -pub(crate) fn input(value: Unknown<'_>) -> BindingResult