diff --git a/.agents/docs/api-alignment-todos.md b/.agents/docs/api-alignment-todos.md index 8f0244f..c4b7c24 100644 --- a/.agents/docs/api-alignment-todos.md +++ b/.agents/docs/api-alignment-todos.md @@ -7,4 +7,3 @@ The direct Taffy binding is implemented. This file contains open work that needs - [ ] Add construction capacity, `print_tree`, asynchronous or off-thread layout, cancellation, or worker transfer only for a concrete consumer need with explicit ownership and failure behavior. - [ ] Re-audit the high-level API, enabled features, numeric values, known panic guards, conversion behavior, and callback failure handling when Taffy or napi-rs changes. - [ ] Decide whether disabling Taffy's internal `calc` feature is useful; calc remains outside the public Style input either way. -- [ ] Decide whether to expose the style surface the pinned Taffy revision adds beyond 0.13.0: `flex-wrap: balance` with its `flexLineCount` companion, and the `min-content`, `max-content`, `fit-content`, `stretch`, and `content` sizing keywords for `size` and `flexBasis`. The binding currently accepts neither, so a stored Style can never hold them. diff --git a/.agents/docs/api-codegen.md b/.agents/docs/api-codegen.md index ec0af3a..3265d0b 100644 --- a/.agents/docs/api-codegen.md +++ b/.agents/docs/api-codegen.md @@ -26,7 +26,7 @@ The existing stages remain separate because they answer different questions: sch The mapping from a primitive shorthand to a complete tagged branch is one shared API fact. A focused versioned input under `api/` must describe the tagged value name, tag field, referenced numeric family, branches, payload fields, public input aliases, and the optional input-only number shorthand. The compiler resolves every family and branch reference against the numeric-family model and rejects an unknown branch, incompatible payload, duplicate public name, or ambiguous shorthand before either target is emitted. This is a separate input family within `tools/api-codegen`, not a property added to `numeric-families.json`: numbers in that file are discriminator codes, while shorthand numbers are payload values. -The initial generated family covers only semantic lengths and available space. It records that a number accepted by `LengthPercentageInput`, `LengthPercentageAutoInput`, or `DimensionInput` maps to the complete `Length(value)` branch, and that a number accepted by `AvailableSpaceInput` maps to `Definite(value)`. `Percent`, `Auto`, `MinContent`, and `MaxContent` remain explicit. Complete tagged inputs remain valid, output types remain complete tagged values, and the generator must not infer the same shorthand for track sizing, Grid values, or another family. +The initial generated family covers only semantic lengths and available space. It records that a number accepted by `LengthPercentageInput`, `LengthPercentageAutoInput`, or `DimensionInput` maps to the complete `Length(value)` branch, and that a number accepted by `AvailableSpaceInput` maps to `Definite(value)`. Other meanings remain explicit: `Dimension` includes percent, auto, intrinsic sizing keywords, length- and percentage-limited fit-content, stretch, and content, while available space includes min-content and max-content. Complete tagged inputs remain valid, output types remain complete tagged values, and the generator must not infer the same shorthand for track sizing, Grid values, or another family. The TypeScript emitter owns the affected public input and output declarations, their JSDoc, and the `Dimension` and `AvailableSpace` complete-form helpers so their names, fields, tags, payloads, and shorthand descriptions come from the same model. Handwritten public entry modules may re-export generated values and types but must not restate their shapes. @@ -36,7 +36,7 @@ Verification follows the repository-wide rule below: `check:codegen` detects sta ## Generated Style codec -`api/style-codec.json` and `api/schemas/style-codec.schema.json` are the maintained versioned model for the 42 public Style input fields, their canonical order, their encoding categories, their referenced numeric families, and their public descriptions. The compiler resolves numeric-family references and derives field indexes and the presence-map width once. Neither target keeps a handwritten second field inventory. +`api/style-codec.json` and `api/schemas/style-codec.schema.json` are the maintained versioned model for the 43 public Style input fields, their canonical order, their encoding categories, their referenced numeric families, and their public descriptions. The compiler resolves numeric-family references and derives field indexes and the presence-map width once. Neither target keeps a handwritten second field inventory. The TypeScript emitter writes `packages/taffyjs-node/src/style-input.ts`, which owns the public `StyleInput` and `StyleUpdate` declarations and a straight-line encoder that reads each known property once in canonical order. The Rust emitter writes `crates/taffyjs_binding/src/style_input.rs`, which applies the matching fields in the same order through `decode_into`. Handwritten `style-codec.ts` and `style_codec.rs` own only the closed category encodings, validation primitives, buffer mechanics, and Taffy-specific conversion used by those generated call sites. diff --git a/.agents/docs/binding-cases.md b/.agents/docs/binding-cases.md index 3efa241..6f382a7 100644 --- a/.agents/docs/binding-cases.md +++ b/.agents/docs/binding-cases.md @@ -107,7 +107,7 @@ This case is complete as an API-alignment example. Its reference value is that t `Style` is one complete owned value. `new_leaf` moves a Style into the new node. `set_style` replaces the node's entire Style and then marks the node dirty; it does not merge the supplied value with the previous Style. `style` returns a borrowed reference to the complete stored Style, and `compute_layout_with_measure` passes a borrowed reference to that same complete value during measurement. -Taffy's examples normally construct a small set of fields and use `..Default::default()` for the rest. The binding currently exposes 42 semantic Style fields after excluding Taffy's Rust-only phantom field and the separately open `flex_line_count` surface. Their transitive types cover booleans, numeric values, optional values, closed keywords and finite containment flags, alignment records, generic `Point`, `Size`, `Rect`, and `Line` records, semantic length variants, nested grid collections, custom grid identifiers, and integer counts and indices. +Taffy's examples normally construct a small set of fields and use `..Default::default()` for the rest. The binding exposes 43 semantic Style fields after excluding Taffy's Rust-only phantom field. Their transitive types cover booleans, numeric values, optional values, closed keywords and finite containment flags, alignment records, generic `Point`, `Size`, `Rect`, and `Line` records, semantic length variants, nested grid collections, custom grid identifiers, and integer counts and indices. The `calc` feature is enabled by Taffy's default features, which this repository keeps enabled, but its public length types represent calc values through opaque pointers. The high-level `TaffyTree` implementation resolves every calc pointer to `0.0`; it does not expose an application resolver. Raw calc pointers therefore cannot define a JavaScript Style value, so the selected public high-level Style vocabulary excludes calc. @@ -229,7 +229,9 @@ The scalar acceptance cases therefore include ordinary rounding and truthful rea ### Selected semantic-length input and output representation -The complete semantic-length form uses a numeric literal discriminator and an ordinary record. Input additionally accepts a direct number as shorthand for the common absolute-length case. The shorthand is equivalent to the complete `Length` record and does not replace `Dimension.Length(value)`. Percent and Auto remain explicit, output always uses the complete tagged form, and CSS strings and Taffy's private compact tags remain outside the public API. `LengthUnit` is a binding-owned numeric family with `Length`, `Percent`, and `Auto` members, using the same stable-code, `EnumValue`, and naming rules as the other closed numeric families. +The complete semantic-length form uses a numeric literal discriminator and an ordinary record. Input additionally accepts a direct number as shorthand for the common absolute-length case. The shorthand is equivalent to the complete `Length` record and does not replace `Dimension.Length(value)`. Every other meaning remains explicit, output always uses the complete tagged form, and CSS strings and Taffy's private compact tags remain outside the public API. + +`LengthUnit` is a binding-owned numeric family with stable codes for `Length`, `Percent`, `Auto`, `MinContent`, `MaxContent`, `FitContent`, `FitContentLength`, `FitContentPercent`, `Stretch`, and `Content`. `LengthPercentage` admits only the first two branches, `LengthPercentageAuto` adds only `Auto`, and `Dimension` admits all ten. This distinction is part of the public field contract: `size` and `flexBasis` use `Dimension`, while `minSize` and `maxSize` remain `LengthPercentageAuto`. The conceptual declarations are: @@ -238,6 +240,13 @@ export const LengthUnit = Object.freeze({ Length: 0, Percent: 1, Auto: 2, + MinContent: 3, + MaxContent: 4, + FitContent: 5, + FitContentLength: 6, + FitContentPercent: 7, + Stretch: 8, + Content: 9, } as const); export type LengthUnit = EnumValue; @@ -258,7 +267,15 @@ export interface AutoInput { export type LengthPercentageInput = number | LengthInput | PercentInput; export type LengthPercentageAutoInput = LengthPercentageInput | AutoInput; -export type DimensionInput = LengthPercentageAutoInput; +export type DimensionInput = + | LengthPercentageAutoInput + | { unit: typeof LengthUnit.MinContent } + | { unit: typeof LengthUnit.MaxContent } + | { unit: typeof LengthUnit.FitContent } + | { unit: typeof LengthUnit.FitContentLength; value: number } + | { unit: typeof LengthUnit.FitContentPercent; value: number } + | { unit: typeof LengthUnit.Stretch } + | { unit: typeof LengthUnit.Content }; export type LengthPercentage = | { @@ -276,12 +293,14 @@ export type LengthPercentageAuto = readonly unit: typeof LengthUnit.Auto; }; -export type Dimension = LengthPercentageAuto; +export type Dimension = Readonly>; ``` -The value-side `Dimension` namespace continues to provide `Dimension.Length(value)`, `Dimension.Percent(value)`, and `Dimension.Auto`. The helpers return the same ordinary records that callers may write directly; they are not native owners or classes. A direct numeric input is only an additive shorthand for `Dimension.Length(value)`. The namespace and members retain the vouched singular PascalCase family style. +The value-side `Dimension` namespace provides `Length(value)`, `Percent(value)`, `Auto`, `MinContent`, `MaxContent`, `FitContent`, `FitContentLength(value)`, `FitContentPercent(value)`, `Stretch`, and `Content`. The helpers return the same ordinary records that callers may write directly; they are not native owners or classes. A direct numeric input is only an additive shorthand for `Dimension.Length(value)`. `FitContentLength` names an application-defined concrete length consistently with the rest of TaffyJS even though the corresponding upstream Rust constructor is named `fit_content_px`. + +Both a direct number and `Dimension.Length(value)` apply the selected ordinary `f64`-to-`f32` conversion and produce the same Taffy absolute length. `Dimension.Percent(50)` and `Dimension.FitContentPercent(50)` convert the user-facing percentage magnitude to Taffy's fractional representation before the final `f32` storage conversion. The reverse mapping reports percentage magnitudes through output. All four numeric-payload branches retain the binding's ordinary negative, `NaN`, infinite, and conversion-overflow behavior. -Both a direct number and `Dimension.Length(value)` apply the selected ordinary `f64`-to-`f32` conversion and produce the same Taffy absolute length. `Dimension.Percent(50)` converts the user-facing percentage magnitude to Taffy's fractional representation before the final `f32` storage conversion. The reverse mapping reports a percentage magnitude through output. The binding does not require a normal range and does not reject negative, `NaN`, or infinite length and percent payloads. Output reports the stored unit and semantic value using readonly versions of the tagged records and does not retain the caller's input form, object, numeric precision, or spelling. Its discriminator remains the same numeric-literal `LengthUnit`, so a caller can use `switch (value.unit)` with ordinary TypeScript narrowing and can pass the output value back into a later Style input. Object identity is not meaningful. Invalid unit codes, missing required length or percent payloads, strings, and other unsupported JavaScript types fail conversion before Style replacement. An extra input property named `value` does not change an Auto variant and is not reproduced in output; the declaration does not add `value?: never` solely to catch that structural extra field. +Output reports the stored unit and semantic value using readonly versions of the tagged records and does not retain the caller's input form, object, numeric precision, or spelling. Its discriminator remains the same numeric-literal `LengthUnit`, so a caller can use `switch (value.unit)` with ordinary TypeScript narrowing and can pass the output value back into a later Style input. Object identity is not meaningful. Invalid unit codes, missing payloads for `Length`, `Percent`, `FitContentLength`, or `FitContentPercent`, strings, intrinsic branches in a narrower length field, and other unsupported JavaScript types fail conversion before Style replacement. An extra numeric `value` does not change a fieldless branch and is not reproduced in output; the declaration does not add `value?: never` solely to catch that structural extra field. `Content` is principally a flex-basis value; Taffy stores it in `size` but specifies that it behaves as `Auto` there. For example, output use remains direct: @@ -291,9 +310,16 @@ const width = tree.getStyle(node).size.width; switch (width.unit) { case LengthUnit.Length: case LengthUnit.Percent: + case LengthUnit.FitContentLength: + case LengthUnit.FitContentPercent: console.log(width.value); break; case LengthUnit.Auto: + case LengthUnit.MinContent: + case LengthUnit.MaxContent: + case LengthUnit.FitContent: + case LengthUnit.Stretch: + case LengthUnit.Content: break; } @@ -302,13 +328,19 @@ tree.setStyle(otherNode, { size: { width } }); Keeping a numeric unit on output makes every returned meaning explicit; it does not require the shorter numeric input to preserve that shape. Logs and JSON therefore show the numeric code already accepted for the closed-enum design. If self-describing serialization later becomes a requirement, it should trigger a reconsideration of the output vocabulary instead of changing the input shorthand. -The input shorthand is unambiguous because the API defines a direct number as an absolute length, while Percent and Auto use objects. Complete tagged records are still necessary for output and for explicitly carrying the unit with its payload. The binding does not use packed numbers, bigint encodings, or raw `CompactLength` bits as public values. +The input shorthand is unambiguous because the API defines a direct number as an absolute length, while every other branch uses an object. Complete tagged records are still necessary for output and for explicitly carrying the unit with its payload. The binding does not use packed numbers, bigint encodings, or raw `CompactLength` bits as public values. -Homogeneous `Rect` and `Size` semantic-length fields accept one contained value and copy it to every component. For example, `padding: 10` and `padding: Dimension.Length(10)` both set all four sides to an absolute length of ten, `margin: Dimension.Percent(5)` sets all four sides to five percent, and `gap: 10` sets both axes. The already selected partial record form remains available for component-specific values. Output always expands to a complete readonly `Rect` or `Size` and never preserves which input form was used. +Homogeneous `Rect` and `Size` semantic-length fields accept one value from that field's own length category and copy it to every component. For example, `padding: 10` and `padding: Dimension.Length(10)` both set all four sides to an absolute length of ten, `margin: Dimension.Percent(5)` sets all four sides to five percent, `gap: 10` sets both axes, and `size: Dimension.MinContent` sets both preferred axes to min-content. The form does not widen the field category: `minSize: Dimension.MinContent` remains invalid. The already selected partial record form remains available for component-specific values. Output always expands to a complete readonly `Rect` or `Size` and never preserves which input form was used. This homogeneous form is not a claim of full CSS shorthand support. The initial binding does not parse CSS text, accept one-to-four-value strings or arrays, or infer analogous scalar meanings for `Line`, grid placement, and other records. A later CSS-facing layer may add those grammars without changing the canonical Taffy value mapping. -Acceptance cases cover numeric shorthand and `Dimension.Length(value)` equivalence, every valid unit and its required payload, `Dimension` helper and direct-object equivalence, 50-to-0.5 percent conversion, actual stored-value output, output-union narrowing by the shared unit tag, direct output-to-input round-trip, negative and non-finite payload pass-through, invalid unit and payload rejection before mutation, unsupported-type rejection, homogeneous `Rect` and `Size` expansion, partial component defaults, and complete readonly aggregate output. +Acceptance cases cover numeric shorthand and `Dimension.Length(value)` equivalence, every valid unit and its required payload, `Dimension` helper and direct-object equivalence, percentage scaling for ordinary and fit-content percentages, actual stored-value output, output-union narrowing by the shared unit tag, direct output-to-input round-trip, negative and non-finite payload pass-through, rejection of intrinsic branches in `minSize` and `maxSize`, invalid unit and payload rejection before mutation, unsupported-type rejection, homogeneous `Rect` and `Size` expansion, partial component defaults, and complete readonly aggregate output. Layout cases prove the intrinsic keywords reach Taffy's Block and Flex algorithms rather than merely round-tripping through Style. + +### Selected balanced flex wrapping + +The public `FlexWrap` numeric family maps `Balance` and `BalanceReverse` directly to Taffy's matching variants, alongside `NoWrap`, `Wrap`, and `WrapReverse`. `flexLineCount` is a complete public Style field stored as Taffy's `u16`; JavaScript input must therefore be an exact integer from 0 through 65,535. Taffy documents values of at least one and defaults to one, while its algorithms normalize zero to one. The binding preserves the representable zero instead of introducing a separate JavaScript clamp. + +Balance requests that Flex items be distributed more evenly across lines; `BalanceReverse` additionally reverses the cross-axis line direction. `flexLineCount` requests the minimum line count for balanced wrapping and also affects cross-axis measurement constraints for ordinary multi-line wrapping, following Taffy's stored semantics. It has no layout effect for `NoWrap`. Acceptance cases cover both balanced variants, exact `u16` bounds, invalid integer input, complete Style readback, update behavior, and a layout whose line distribution differs because of the requested count. ### Selected alignment representation diff --git a/.agents/docs/binding-mapping.md b/.agents/docs/binding-mapping.md index e79400f..9c9769e 100644 --- a/.agents/docs/binding-mapping.md +++ b/.agents/docs/binding-mapping.md @@ -58,11 +58,15 @@ Indices and Rust integer payloads must be finite exact integers in their public Fieldless families such as `Display`, `Overflow`, and `AlignItems` use stable numeric literal members exposed through frozen PascalCase objects. Public code should use the names, while an exact valid raw code remains accepted. Rust conversion checks exact family membership and does not derive codes from Rust declaration order. +`FlexWrap` includes Taffy's balanced and balanced-reverse variants. The companion `flexLineCount` field is checked as an exact `u16`; the binding preserves every representable value, including zero, while Taffy's layout algorithms interpret zero as one. + `Contain` uses the same finite numeric-family boundary for the four combinations Taffy can store: `None`, `Layout`, `Paint`, and `Content` (`Layout | Paint`). This exposes Taffy's layout-affecting containment flags without importing CSS parsing or accepting unknown bits. ### Lengths, available space, and other tagged values -Length inputs accept a direct number as shorthand for an absolute length. The complete tagged form remains supported through values such as `Dimension.Length(20)`, `Dimension.Percent(50)`, and `Dimension.Auto`, and tagged length outputs remain valid later inputs. Percent helpers use user-facing percentages, so `50` maps to Taffy's `0.5`. CSS strings are not length values. +Length inputs accept a direct number as shorthand for an absolute length. The complete tagged form remains supported through values such as `Dimension.Length(20)`, `Dimension.Percent(50)`, and `Dimension.Auto`, and tagged length outputs remain valid later inputs. Full `Dimension` values also include `MinContent`, `MaxContent`, bare `FitContent`, `FitContentLength(value)`, `FitContentPercent(value)`, `Stretch`, and `Content`. Percent helpers use user-facing percentages, so `50` maps to Taffy's `0.5`. CSS strings are not length values. + +Only `size` and `flexBasis` accept that full `Dimension` family. `minSize` and `maxSize` remain `LengthPercentageAuto`; spacing fields retain their narrower length categories. `Content` is principally a flex-basis value and is stored unchanged in `size`, where Taffy defines it to behave like `Auto`. Available-space inputs accept a direct number as shorthand for `Definite`. The complete forms remain supported through `AvailableSpace.Definite(value)`, `AvailableSpace.MinContent`, and `AvailableSpace.MaxContent`, and tagged available-space outputs remain valid later inputs. Every JavaScript number, including negative values, `NaN`, and infinities, retains the existing definite-value conversion behavior; no number is reserved for a special variant. diff --git a/.agents/docs/style-codec.md b/.agents/docs/style-codec.md index 2b7f161..65d05cc 100644 --- a/.agents/docs/style-codec.md +++ b/.agents/docs/style-codec.md @@ -10,13 +10,13 @@ The generated encoder reads each known top-level property once through ordinary ## Generated ownership -`api/style-codec.json` is the canonical versioned inventory for all 42 fields, their order, encoding categories, referenced numeric families, and public descriptions; `api/schemas/style-codec.schema.json` closes that vocabulary. `tools/api-codegen` validates and compiles the model, then generates `packages/taffyjs-node/src/style-input.ts` and `crates/taffyjs_binding/src/style_input.rs`. The TypeScript output owns `StyleInput`, `StyleUpdate`, and the straight-line property encoder. The Rust output owns the matching straight-line `decode_into(target, encoded) -> Result` field application. Handwritten codec modules implement the finite encoding categories rather than interpreting schema metadata at runtime. +`api/style-codec.json` is the canonical versioned inventory for all 43 fields, their order, encoding categories, referenced numeric families, and public descriptions; `api/schemas/style-codec.schema.json` closes that vocabulary. `tools/api-codegen` validates and compiles the model, then generates `packages/taffyjs-node/src/style-input.ts` and `crates/taffyjs_binding/src/style_input.rs`. The TypeScript output owns `StyleInput`, `StyleUpdate`, and the straight-line property encoder. The Rust output owns the matching straight-line `decode_into(target, encoded) -> Result` field application. Handwritten codec modules implement the finite encoding categories rather than interpreting schema metadata at runtime. -## Wire version 2 +## Wire version 3 -Every encoded value begins with two magic bytes (`0x54`, `0x53`), one wire-version byte, one presence-width byte, and the fixed-width presence bitmap. Version 2 has 42 fields and therefore six presence bytes. It replaced version 1 when Taffy's `contain` field entered the canonical order and shifted later field indexes. Set bits identify payloads that follow in the canonical model order; unset bits carry no payload. The decoder rejects bad magic, a different version or presence width, bits beyond the known field count, truncation, and trailing data. +Every encoded value begins with two magic bytes (`0x54`, `0x53`), one wire-version byte, one presence-width byte, and the fixed-width presence bitmap. Version 3 has 43 fields and therefore six presence bytes. `flexLineCount` follows `flexWrap` in canonical Taffy order, so its addition shifted every later field index and required this version. Set bits identify payloads that follow in the canonical model order; unset bits carry no payload. The decoder rejects bad magic, a different version or presence width, bits beyond the known field count, truncation, and trailing data. -Payload integers and IEEE-754 values are little-endian. Booleans, enum discriminators, nullable markers, tagged-union discriminators, and nested component masks use bytes; all public discriminator assignments are consumed through the TypeScript constants and Rust code enums generated from `api/numeric-families.json`, rather than repeated as codec literals. Ordinary numbers use `f64` and undergo the existing Rust `f64`-to-`f32` conversion; Grid indexes and counts use their checked `i16`, `u16`, or `u32` widths. Lengths carry a unit byte and a number only for branches that need one. Strings carry a `u32` UTF-8 byte length followed by bytes, and collections carry a `u32` element count followed by recursively encoded complete elements. Partial geometry carries a component mask, with a reserved scalar-expansion marker where the public shorthand permits it. +Payload integers and IEEE-754 values are little-endian. Booleans, enum discriminators, nullable markers, tagged-union discriminators, and nested component masks use bytes; all public discriminator assignments are consumed through the TypeScript constants and Rust code enums generated from `api/numeric-families.json`, rather than repeated as codec literals. Ordinary numbers use `f64` and undergo the existing Rust `f64`-to-`f32` conversion; `flexLineCount`, Grid indexes, and counts use their checked `i16`, `u16`, or `u32` widths. Lengths carry a unit byte and a number only for branches that need one. Strings carry a `u32` UTF-8 byte length followed by bytes, and collections carry a `u32` element count followed by recursively encoded complete elements. Partial geometry carries a component mask, with a reserved scalar-expansion marker where the public shorthand permits it. The decoder borrows the byte slice and reserves fallible Rust collection capacity from validated counts. It applies values to a local default or cloned candidate while preserving bitwise change detection for `NaN`, `+0`, and `-0`, validates the complete candidate, and exposes it to the tree only after successful completion. Both sides are generated straight-line code over one fixed field order: neither interprets schema metadata at runtime, and no decoded Style value is retained between calls. @@ -38,4 +38,4 @@ Acceptance performance is measured through the complete public API rather than b ## Verification boundary -Shared public behavior tests run against both `@taffyjs/node` and `@taffyjs/wasm` and cover all five operations, defaults, replacement, partial update, absence, null, empty collections, every Grid/string family, unknown and invalid fields, atomic failure, unchanged updates, floating-point edge cases, NodeId representation precedence, getters, Proxies, reentrancy, children, and context. Rust safety tests exercise malformed headers, versions, bitmaps, lengths, counts, UTF-8, truncation, and trailing bytes without freezing a particular byte snapshot. Two guards run over the complete public field list rather than over a sample: every field accepts explicit `undefined` as absence, and exactly the eight publicly nullable fields accept `null` while the other thirty-four reject it. Generated freshness is enforced by `vp run check:codegen` in a clean checkout. +Shared public behavior tests run against both `@taffyjs/node` and `@taffyjs/wasm` and cover all five operations, defaults, replacement, partial update, absence, null, empty collections, every Grid/string family, unknown and invalid fields, atomic failure, unchanged updates, floating-point edge cases, NodeId representation precedence, getters, Proxies, reentrancy, children, and context. Rust safety tests exercise malformed headers, versions, bitmaps, lengths, counts, UTF-8, truncation, and trailing bytes without freezing a particular byte snapshot. Two guards run over the complete public field list rather than over a sample: every field accepts explicit `undefined` as absence, and exactly the eight publicly nullable fields accept `null` while the other thirty-five reject it. Generated freshness is enforced by `vp run check:codegen` in a clean checkout. diff --git a/.agents/docs/taffyjs-node-decisions.md b/.agents/docs/taffyjs-node-decisions.md index b3da63a..f25af2e 100644 --- a/.agents/docs/taffyjs-node-decisions.md +++ b/.agents/docs/taffyjs-node-decisions.md @@ -260,6 +260,16 @@ New public state owners, compatibility layers, retained JavaScript values, callb **Source:** Yunfei (`@hyfdev`), 2026-08-16 and 2026-08-17; first required the Apple arm64 benchmark host and broader napi-rs parity, then explicitly chose the maintained templates' 13 native targets after comparing that baseline with napi-rs core and Rolldown coverage. +### Intrinsic sizing and balanced flex wrapping + +**Ruling:** The public Tree Style must expose Taffy's full non-calc `Dimension` surface for `size` and `flexBasis`, including min-content, max-content, bare and length- or percentage-limited fit-content, stretch, and content. It must also expose `FlexWrap.Balance`, `FlexWrap.BalanceReverse`, and `flexLineCount`. + +**Limits:** `minSize` and `maxSize` remain `LengthPercentageAuto` and must not accept intrinsic sizing keywords. This ruling does not expose calc pointers, CSS strings, Rust helper methods, or capabilities outside the public Tree Style. `Content` remains storable in `size`, where upstream Taffy specifies that it behaves as auto outside `flexBasis`. + +**Why:** Yunfei identified these as the missing public Tree layout capabilities in scope and instructed that all of them be exposed. No additional rationale was stated. + +**Source:** Yunfei (`@hyfdev`), 2026-08-23; explicit instruction to expose the enumerated capabilities after narrowing the review to the public Tree surface. See [semantic-length mapping](binding-cases.md#selected-semantic-length-input-and-output-representation) and the canonical [tagged values](../../api/tagged-values.json) and [Style fields](../../api/style-codec.json). + ### Generated numeric input shorthand [VOUCHED @hyfdev 2026-08-15] diff --git a/api/numeric-families.json b/api/numeric-families.json index a714395..7a83151 100644 --- a/api/numeric-families.json +++ b/api/numeric-families.json @@ -91,7 +91,9 @@ "members": [ { "name": "NoWrap", "value": 0 }, { "name": "Wrap", "value": 1 }, - { "name": "WrapReverse", "value": 2 } + { "name": "WrapReverse", "value": 2 }, + { "name": "Balance", "value": 3 }, + { "name": "BalanceReverse", "value": 4 } ] }, { @@ -148,7 +150,14 @@ "members": [ { "name": "Length", "value": 0 }, { "name": "Percent", "value": 1 }, - { "name": "Auto", "value": 2 } + { "name": "Auto", "value": 2 }, + { "name": "MinContent", "value": 3 }, + { "name": "MaxContent", "value": 4 }, + { "name": "FitContent", "value": 5 }, + { "name": "FitContentLength", "value": 6 }, + { "name": "FitContentPercent", "value": 7 }, + { "name": "Stretch", "value": 8 }, + { "name": "Content", "value": 9 } ] }, { diff --git a/api/schemas/style-codec.schema.json b/api/schemas/style-codec.schema.json index 152fca0..fb47b15 100644 --- a/api/schemas/style-codec.schema.json +++ b/api/schemas/style-codec.schema.json @@ -39,8 +39,10 @@ "partial-point-enum", "partial-rect-length-percentage-auto", "partial-size-dimension", + "partial-size-length-percentage-auto", "partial-rect-length-percentage", "partial-size-length-percentage", + "unsigned-16", "dimension", "grid-template-component-array", "track-sizing-array", diff --git a/api/style-codec.json b/api/style-codec.json index 4eefba4..f873a69 100644 --- a/api/style-codec.json +++ b/api/style-codec.json @@ -1,7 +1,7 @@ { "$schema": "./schemas/style-codec.schema.json", "formatVersion": 1, - "wireVersion": 2, + "wireVersion": 3, "fields": [ { "name": "display", @@ -78,12 +78,12 @@ }, { "name": "minSize", - "category": "partial-size-dimension", + "category": "partial-size-length-percentage-auto", "description": "Supplies the node's min size style when present." }, { "name": "maxSize", - "category": "partial-size-dimension", + "category": "partial-size-length-percentage-auto", "description": "Supplies the node's max size style when present." }, { @@ -165,6 +165,11 @@ "numericFamily": "FlexWrap", "description": "Supplies the node's flex wrap style when present." }, + { + "name": "flexLineCount", + "category": "unsigned-16", + "description": "Supplies the requested minimum flex line count as an exact integer from 0 through 65535 when present." + }, { "name": "flexBasis", "category": "dimension", diff --git a/api/tagged-values.json b/api/tagged-values.json index 60dc219..19b66dc 100644 --- a/api/tagged-values.json +++ b/api/tagged-values.json @@ -19,7 +19,20 @@ "publicInput": "PercentInput", "payload": { "name": "value", "type": "number" } }, - { "name": "Auto", "publicInput": "AutoInput" } + { "name": "Auto", "publicInput": "AutoInput" }, + { "name": "MinContent" }, + { "name": "MaxContent" }, + { "name": "FitContent" }, + { + "name": "FitContentLength", + "payload": { "name": "value", "type": "number" } + }, + { + "name": "FitContentPercent", + "payload": { "name": "value", "type": "number" } + }, + { "name": "Stretch" }, + { "name": "Content" } ], "publicAliases": [ { @@ -34,7 +47,18 @@ }, { "name": "Dimension", - "branches": ["Length", "Percent", "Auto"], + "branches": [ + "Length", + "Percent", + "Auto", + "MinContent", + "MaxContent", + "FitContent", + "FitContentLength", + "FitContentPercent", + "Stretch", + "Content" + ], "acceptsNumber": true } ] diff --git a/apps/website/guide/flexbox.md b/apps/website/guide/flexbox.md index 2dcd344..b116f32 100644 --- a/apps/website/guide/flexbox.md +++ b/apps/website/guide/flexbox.md @@ -45,9 +45,11 @@ The children are the same nodes, in the same order, with the same styles. The ch ## The related controls -`flexBasis` supplies the starting main-axis size when present. `flexGrow` divides positive free space. `flexShrink` controls how items contract when their bases do not fit. `size`, `minSize`, and `maxSize` still constrain the result. +`flexBasis` supplies the starting main-axis size when present. Use `Dimension.Content` to derive that basis from the item's measured content. `flexGrow` divides positive free space. `flexShrink` controls how items contract when their bases do not fit. `size`, `minSize`, and `maxSize` still constrain the result. -`FlexWrap.Wrap` allows items to form additional lines when they do not fit on one line; `WrapReverse` reverses the cross-axis line direction. Without wrapping, `FlexWrap.NoWrap` keeps one line. +`FlexWrap.Wrap` allows items to form additional lines when they do not fit on one line; `WrapReverse` reverses the cross-axis line direction. `FlexWrap.Balance` distributes items more evenly across lines, and `BalanceReverse` combines that distribution with reversed cross-axis line direction. Without wrapping, `FlexWrap.NoWrap` keeps one line. + +`flexLineCount` requests at least that many lines for balanced wrapping. For any multi-line wrapping mode, it also tells Taffy how many lines to use when dividing definite cross-axis space for measurement. The default is `1`; use a positive integer up to 65,535. It has no layout effect with `FlexWrap.NoWrap`. Alignment names describe where remaining space goes: diff --git a/apps/website/guide/styles-and-values.md b/apps/website/guide/styles-and-values.md index 688009e..24dbaf6 100644 --- a/apps/website/guide/styles-and-values.md +++ b/apps/website/guide/styles-and-values.md @@ -83,7 +83,7 @@ const style = { The values are numbers at runtime. Exact valid raw numeric codes are accepted as a low-level form, but named constants are the intended way to write them: they show the meaning and retain useful TypeScript types. -Values that carry data use ordinary tagged objects. A number is the concise input form for a concrete length, so `20` is equivalent to `Dimension.Length(20)`. Percent and automatic sizing remain explicit as `Dimension.Percent(50)` and `Dimension.Auto`. A numeric available-space component is likewise equivalent to `AvailableSpace.Definite(value)`, while `AvailableSpace.MinContent` and `AvailableSpace.MaxContent` stay explicit. The complete helpers remain valid, and outputs always use complete tagged objects. Grid has its own placement and track helpers, introduced in [Grid](./grid.md). +Values that carry data use ordinary tagged objects. A number is the concise input form for a concrete length, so `20` is equivalent to `Dimension.Length(20)`. Other sizing meanings stay explicit: `Dimension.Percent(50)`, `Dimension.Auto`, `Dimension.MinContent`, `Dimension.MaxContent`, `Dimension.FitContent`, `Dimension.FitContentLength(value)`, `Dimension.FitContentPercent(value)`, `Dimension.Stretch`, and `Dimension.Content`. The complete `Dimension` family is valid for `size` and `flexBasis`; `minSize` and `maxSize` remain limited to concrete lengths, percentages, and auto. A numeric available-space component is likewise equivalent to `AvailableSpace.Definite(value)`, while `AvailableSpace.MinContent` and `AvailableSpace.MaxContent` stay explicit. The complete helpers remain valid, and outputs always use complete tagged objects. Grid has its own placement and track helpers, introduced in [Grid](./grid.md). ## Outputs describe stored values diff --git a/apps/website/node/style.md b/apps/website/node/style.md index 14c5260..0c8b762 100644 --- a/apps/website/node/style.md +++ b/apps/website/node/style.md @@ -17,6 +17,8 @@ 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. | +`size` accepts every `Dimension` helper, including intrinsic sizing, fit-content, stretch, and content. `minSize` and `maxSize` accept only concrete lengths, percentages, and `Dimension.Auto`. + 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`. @@ -32,10 +34,11 @@ Block layout also uses the shared sizing, box-model, overflow, and positioning f ## Flexbox fields -| Fields | Purpose | -| ------------------------------------- | ------------------------------------------------------ | -| `flexDirection`, `flexWrap` | Select the main axis and whether items form new lines. | -| `flexBasis`, `flexGrow`, `flexShrink` | Set the starting main size and distribute free space. | +| Fields | Purpose | +| ------------------------------------- | ------------------------------------------------------------------ | +| `flexDirection`, `flexWrap` | Select the main axis and how items form and order lines. | +| `flexLineCount` | Request a minimum line count and influence multi-line measurement. | +| `flexBasis`, `flexGrow`, `flexShrink` | Set the starting main size and distribute free space. | The shared alignment fields control placement along the main and cross axes. See the [Flexbox guide](../guide/flexbox.md) for how these fields work together. diff --git a/apps/website/node/value-helpers.md b/apps/website/node/value-helpers.md index ed68ba1..2d398fe 100644 --- a/apps/website/node/value-helpers.md +++ b/apps/website/node/value-helpers.md @@ -39,6 +39,11 @@ Invalid codes throw `RangeError`. Values are not coerced from strings, booleans, - `Dimension.Length(value)` is a concrete length. - `Dimension.Percent(value)` uses a percentage magnitude, so `50` means 50 percent. - `Dimension.Auto` leaves the size automatic. +- `Dimension.MinContent` and `Dimension.MaxContent` select intrinsic content sizes. +- `Dimension.FitContent` uses the available-space fit-content formula. +- `Dimension.FitContentLength(value)` and `Dimension.FitContentPercent(value)` cap fit-content with a concrete or percentage limit. +- `Dimension.Stretch` uses the stretch-fit size. +- `Dimension.Content` asks Flexbox to base `flexBasis` on content; in `size`, Taffy treats it like `Auto`. `AvailableSpace` describes a computation constraint: @@ -68,11 +73,20 @@ const width = tree.getStyle(node).size.width; switch (width.unit) { case LengthUnit.Length: case LengthUnit.Percent: + case LengthUnit.FitContentLength: + case LengthUnit.FitContentPercent: console.log(width.value); break; case LengthUnit.Auto: + case LengthUnit.MinContent: + case LengthUnit.MaxContent: + case LengthUnit.FitContent: + case LengthUnit.Stretch: + case LengthUnit.Content: break; } ``` +The full `Dimension` family is accepted by `size` and `flexBasis`. `minSize` and `maxSize` deliberately remain limited to concrete lengths, percentages, and `Auto`. + The generated declaration file is the exhaustive source for every numeric member and tagged TypeScript shape. This page groups the exports by how they are used rather than duplicating that declaration. diff --git a/crates/taffyjs_binding/src/length.rs b/crates/taffyjs_binding/src/length.rs index 69b5947..1330e23 100644 --- a/crates/taffyjs_binding/src/length.rs +++ b/crates/taffyjs_binding/src/length.rs @@ -1,5 +1,7 @@ use napi_derive::napi; -use taffy::style::{CompactLength, Dimension, LengthPercentage, LengthPercentageAuto}; +use taffy::style::{ + CompactLength, Dimension, ExpandedDimension, LengthPercentage, LengthPercentageAuto, +}; use crate::numeric::LengthUnitCode; @@ -28,7 +30,49 @@ fn output(raw: CompactLength) -> LengthOutput { } pub(crate) fn dimension_output(value: Dimension) -> LengthOutput { - output(value.into_raw()) + match value.expand() { + ExpandedDimension::Length(value) => LengthOutput { + unit: LengthUnitCode::Length as u8, + value: Some(f64::from(value)), + }, + ExpandedDimension::Percent(value) => LengthOutput { + unit: LengthUnitCode::Percent as u8, + value: Some(f64::from(value) * 100.0), + }, + ExpandedDimension::Auto => LengthOutput { + unit: LengthUnitCode::Auto as u8, + value: None, + }, + ExpandedDimension::MinContent => LengthOutput { + unit: LengthUnitCode::MinContent as u8, + value: None, + }, + ExpandedDimension::MaxContent => LengthOutput { + unit: LengthUnitCode::MaxContent as u8, + value: None, + }, + ExpandedDimension::FitContent => LengthOutput { + unit: LengthUnitCode::FitContent as u8, + value: None, + }, + ExpandedDimension::FitContentPx(value) => LengthOutput { + unit: LengthUnitCode::FitContentLength as u8, + value: Some(f64::from(value)), + }, + ExpandedDimension::FitContentPercent(value) => LengthOutput { + unit: LengthUnitCode::FitContentPercent as u8, + value: Some(f64::from(value) * 100.0), + }, + ExpandedDimension::Stretch => LengthOutput { + unit: LengthUnitCode::Stretch as u8, + value: None, + }, + ExpandedDimension::Content => LengthOutput { + unit: LengthUnitCode::Content as u8, + value: None, + }, + ExpandedDimension::Calc(_) => panic!("unsupported Taffy calc dimension"), + } } pub(crate) fn length_percentage_output(value: LengthPercentage) -> LengthOutput { @@ -53,5 +97,14 @@ mod tests { let auto = dimension_output(Dimension::auto()); assert_eq!((auto.unit, auto.value), (2, None)); + + let fit_content_percent = dimension_output(Dimension::fit_content_percent(0.25)); + assert_eq!( + (fit_content_percent.unit, fit_content_percent.value), + (7, Some(25.0)) + ); + + let content = dimension_output(Dimension::content()); + assert_eq!((content.unit, content.value), (9, None)); } } diff --git a/crates/taffyjs_binding/src/numeric.rs b/crates/taffyjs_binding/src/numeric.rs index 10af6b4..896374e 100644 --- a/crates/taffyjs_binding/src/numeric.rs +++ b/crates/taffyjs_binding/src/numeric.rs @@ -91,6 +91,8 @@ numeric_codes! { NoWrap = 0, Wrap = 1, WrapReverse = 2, + Balance = 3, + BalanceReverse = 4, } enum GridAutoFlowCode { Row = 0, @@ -136,6 +138,13 @@ numeric_codes! { Length = 0, Percent = 1, Auto = 2, + MinContent = 3, + MaxContent = 4, + FitContent = 5, + FitContentLength = 6, + FitContentPercent = 7, + Stretch = 8, + Content = 9, } enum AvailableSpaceKindCode { Definite = 0, diff --git a/crates/taffyjs_binding/src/style.rs b/crates/taffyjs_binding/src/style.rs index c92b745..efa4f00 100644 --- a/crates/taffyjs_binding/src/style.rs +++ b/crates/taffyjs_binding/src/style.rs @@ -71,6 +71,7 @@ pub struct StyleOutput { pub text_align: u8, pub flex_direction: u8, pub flex_wrap: u8, + pub flex_line_count: u16, pub flex_basis: length::LengthOutput, pub flex_grow: f64, pub flex_shrink: f64, @@ -222,6 +223,8 @@ pub(crate) fn flex_wrap(value: f64) -> BindingResult { FlexWrapCode::NoWrap => FlexWrap::NoWrap, FlexWrapCode::Wrap => FlexWrap::Wrap, FlexWrapCode::WrapReverse => FlexWrap::WrapReverse, + FlexWrapCode::Balance => FlexWrap::Balance, + FlexWrapCode::BalanceReverse => FlexWrap::BalanceReverse, }) } @@ -387,7 +390,8 @@ fn flex_wrap_output(value: FlexWrap) -> u8 { FlexWrap::NoWrap => FlexWrapCode::NoWrap as u8, FlexWrap::Wrap => FlexWrapCode::Wrap as u8, FlexWrap::WrapReverse => FlexWrapCode::WrapReverse as u8, - FlexWrap::Balance | FlexWrap::BalanceReverse => panic!("unsupported Taffy flex wrap"), + FlexWrap::Balance => FlexWrapCode::Balance as u8, + FlexWrap::BalanceReverse => FlexWrapCode::BalanceReverse as u8, } } @@ -477,6 +481,7 @@ pub(crate) fn output(style: &Style) -> StyleOutput { text_align: text_align_output(style.text_align), flex_direction: flex_direction_output(style.flex_direction), flex_wrap: flex_wrap_output(style.flex_wrap), + flex_line_count: style.flex_line_count, flex_basis: length::dimension_output(style.flex_basis), flex_grow: f64::from(style.flex_grow), flex_shrink: f64::from(style.flex_shrink), diff --git a/crates/taffyjs_binding/src/style_codec.rs b/crates/taffyjs_binding/src/style_codec.rs index 8287327..13d2ad2 100644 --- a/crates/taffyjs_binding/src/style_codec.rs +++ b/crates/taffyjs_binding/src/style_codec.rs @@ -45,6 +45,13 @@ enum EncodedLength { Length(f64), Percent(f64), Auto, + MinContent, + MaxContent, + FitContent, + FitContentLength(f64), + FitContentPercent(f64), + Stretch, + Content, } fn reserved_vec(count: usize, name: &str) -> BindingResult> { @@ -188,24 +195,42 @@ impl<'a> StyleDecoder<'a> { Ok(changed) } - pub(crate) fn dimension_size>( + pub(crate) fn dimension_size( &mut self, - current: &mut Size, + current: &mut Size, name: &str, ) -> BindingResult { let mask = self.geometry_mask(0b11, true, name)?; if mask == SCALAR_GEOMETRY { - let value = self.length_percentage_auto(name)?.into(); + let value = self.dimension(name)?; return Ok(replace(&mut current.width, value) | replace(&mut current.height, value)); } let mut changed = false; if mask & 1 != 0 { - let value = self.length_percentage_auto(name)?.into(); - changed |= replace(&mut current.width, value); + changed |= replace(&mut current.width, self.dimension(name)?); } if mask & 2 != 0 { - let value = self.length_percentage_auto(name)?.into(); - changed |= replace(&mut current.height, value); + changed |= replace(&mut current.height, self.dimension(name)?); + } + Ok(changed) + } + + pub(crate) fn length_percentage_auto_size( + &mut self, + current: &mut Size, + name: &str, + ) -> BindingResult { + let mask = self.geometry_mask(0b11, true, name)?; + if mask == SCALAR_GEOMETRY { + let value = self.length_percentage_auto(name)?; + return Ok(replace(&mut current.width, value) | replace(&mut current.height, value)); + } + let mut changed = false; + if mask & 1 != 0 { + changed |= replace(&mut current.width, self.length_percentage_auto(name)?); + } + if mask & 2 != 0 { + changed |= replace(&mut current.height, self.length_percentage_auto(name)?); } Ok(changed) } @@ -260,13 +285,28 @@ impl<'a> StyleDecoder<'a> { } pub(crate) fn dimension(&mut self, name: &str) -> BindingResult { - Ok(match self.encoded_length(true, name)? { + Ok(match self.encoded_length(true, true, name)? { EncodedLength::Length(value) => Dimension::length(number::to_f32(value)), EncodedLength::Percent(value) => Dimension::percent(number::to_f32(value / 100.0)), EncodedLength::Auto => Dimension::auto(), + EncodedLength::MinContent => Dimension::min_content(), + EncodedLength::MaxContent => Dimension::max_content(), + EncodedLength::FitContent => Dimension::fit_content(), + EncodedLength::FitContentLength(value) => { + Dimension::fit_content_px(number::to_f32(value)) + } + EncodedLength::FitContentPercent(value) => { + Dimension::fit_content_percent(number::to_f32(value / 100.0)) + } + EncodedLength::Stretch => Dimension::stretch(), + EncodedLength::Content => Dimension::content(), }) } + pub(crate) fn unsigned_16(&mut self, name: &str) -> BindingResult { + self.u16(name) + } + pub(crate) fn grid_template_components( &mut self, name: &str, @@ -350,31 +390,50 @@ impl<'a> StyleDecoder<'a> { } fn length_percentage(&mut self, name: &str) -> BindingResult { - Ok(match self.encoded_length(false, name)? { + Ok(match self.encoded_length(false, false, name)? { EncodedLength::Length(value) => LengthPercentage::length(number::to_f32(value)), EncodedLength::Percent(value) => { LengthPercentage::percent(number::to_f32(value / 100.0)) } EncodedLength::Auto => return Err(type_error(format!("{name} cannot be Auto"))), + _ => return Err(type_error(format!("{name} has an invalid length tag"))), }) } fn length_percentage_auto(&mut self, name: &str) -> BindingResult { - Ok(match self.encoded_length(true, name)? { + Ok(match self.encoded_length(true, false, name)? { EncodedLength::Length(value) => LengthPercentageAuto::length(number::to_f32(value)), EncodedLength::Percent(value) => { LengthPercentageAuto::percent(number::to_f32(value / 100.0)) } EncodedLength::Auto => LengthPercentageAuto::auto(), + _ => return Err(type_error(format!("{name} has an invalid length tag"))), }) } - fn encoded_length(&mut self, allow_auto: bool, name: &str) -> BindingResult { + fn encoded_length( + &mut self, + allow_auto: bool, + allow_intrinsic: bool, + name: &str, + ) -> BindingResult { match i64::from(self.u8(name)?).try_into() { Ok(LengthUnitCode::Length) => Ok(EncodedLength::Length(self.f64(name)?)), Ok(LengthUnitCode::Percent) => Ok(EncodedLength::Percent(self.f64(name)?)), Ok(LengthUnitCode::Auto) if allow_auto => Ok(EncodedLength::Auto), Ok(LengthUnitCode::Auto) => Err(type_error(format!("{name} cannot be Auto"))), + Ok(LengthUnitCode::MinContent) if allow_intrinsic => Ok(EncodedLength::MinContent), + Ok(LengthUnitCode::MaxContent) if allow_intrinsic => Ok(EncodedLength::MaxContent), + Ok(LengthUnitCode::FitContent) if allow_intrinsic => Ok(EncodedLength::FitContent), + Ok(LengthUnitCode::FitContentLength) if allow_intrinsic => { + Ok(EncodedLength::FitContentLength(self.f64(name)?)) + } + Ok(LengthUnitCode::FitContentPercent) if allow_intrinsic => { + Ok(EncodedLength::FitContentPercent(self.f64(name)?)) + } + Ok(LengthUnitCode::Stretch) if allow_intrinsic => Ok(EncodedLength::Stretch), + Ok(LengthUnitCode::Content) if allow_intrinsic => Ok(EncodedLength::Content), + Ok(_) => Err(type_error(format!("{name} has an invalid length tag"))), Err(_) => Err(type_error(format!("{name} has an invalid length tag"))), } } @@ -590,7 +649,7 @@ mod tests { const PRESENCE_BYTES: usize = 6; fn packet(field: Option, payload: &[u8]) -> Vec { - let mut encoded = vec![STYLE_MAGIC_0, STYLE_MAGIC_1, 2, PRESENCE_BYTES as u8]; + let mut encoded = vec![STYLE_MAGIC_0, STYLE_MAGIC_1, 3, PRESENCE_BYTES as u8]; encoded.resize(4 + PRESENCE_BYTES, 0); if let Some(field) = field { encoded[4 + (field >> 3)] |= 1 << (field & 7); @@ -604,18 +663,18 @@ mod tests { for encoded in [ Vec::new(), vec![STYLE_MAGIC_0], - vec![0, STYLE_MAGIC_1, 2, PRESENCE_BYTES as u8], - vec![STYLE_MAGIC_0, STYLE_MAGIC_1, 1, PRESENCE_BYTES as u8], - vec![STYLE_MAGIC_0, STYLE_MAGIC_1, 2, 0], + vec![0, STYLE_MAGIC_1, 3, PRESENCE_BYTES as u8], + vec![STYLE_MAGIC_0, STYLE_MAGIC_1, 2, PRESENCE_BYTES as u8], + vec![STYLE_MAGIC_0, STYLE_MAGIC_1, 3, 0], ] { - assert!(StyleDecoder::new(&encoded, 2, PRESENCE_BYTES, 42).is_err()); + assert!(StyleDecoder::new(&encoded, 3, PRESENCE_BYTES, 43).is_err()); } } #[test] fn rejects_truncation_trailing_data_and_unknown_presence_bits() { let mut target = Style::default(); - assert!(style_input::decode_into(&mut target, &packet(Some(30), &[])).is_err()); + assert!(style_input::decode_into(&mut target, &packet(Some(31), &[])).is_err()); assert!(style_input::decode_into(&mut target, &packet(None, &[0])).is_err()); assert!(style_input::decode_into(&mut target, &packet(Some(47), &[])).is_err()); } diff --git a/crates/taffyjs_binding/src/style_input.rs b/crates/taffyjs_binding/src/style_input.rs index 1898d87..89dcf1c 100644 --- a/crates/taffyjs_binding/src/style_input.rs +++ b/crates/taffyjs_binding/src/style_input.rs @@ -9,7 +9,7 @@ use crate::style; use crate::style_codec::{StyleDecoder, replace, replace_f32, replace_optional_f32}; pub(crate) fn decode_into(target: &mut Style, encoded: &[u8]) -> BindingResult { - let mut decoder = StyleDecoder::new(encoded, 2, 6, 42)?; + let mut decoder = StyleDecoder::new(encoded, 3, 6, 43)?; let mut changed = false; if decoder.field(0) { let value = style::display(f64::from(decoder.enumeration("Style.display")?))?; @@ -67,10 +67,10 @@ pub(crate) fn decode_into(target: &mut Style, encoded: &[u8]) -> BindingResult BindingResult; type FlexWrap = EnumValue; /** Lists the supported grid auto flow choices as stable numeric constants. */ @@ -207,6 +211,20 @@ declare const LengthUnit: Readonly<{ readonly Percent: 1; /** Selects the Auto choice from the LengthUnit numeric family. */ readonly Auto: 2; + /** Selects the MinContent choice from the LengthUnit numeric family. */ + readonly MinContent: 3; + /** Selects the MaxContent choice from the LengthUnit numeric family. */ + readonly MaxContent: 4; + /** Selects the FitContent choice from the LengthUnit numeric family. */ + readonly FitContent: 5; + /** Selects the FitContentLength choice from the LengthUnit numeric family. */ + readonly FitContentLength: 6; + /** Selects the FitContentPercent choice from the LengthUnit numeric family. */ + readonly FitContentPercent: 7; + /** Selects the Stretch choice from the LengthUnit numeric family. */ + readonly Stretch: 8; + /** Selects the Content choice from the LengthUnit numeric family. */ + readonly Content: 9; }>; type LengthUnit = EnumValue; /** Lists the supported available space kind choices as stable numeric constants. */ @@ -300,6 +318,36 @@ type PercentInput = { type AutoInput = { /** Selects the Auto branch. */ unit: typeof LengthUnit.Auto; }; +/** Supplies the complete `Dimension.MinContent` input form. */ +type DimensionMinContentInput = { + /** Selects the MinContent branch. */ unit: typeof LengthUnit.MinContent; +}; +/** Supplies the complete `Dimension.MaxContent` input form. */ +type DimensionMaxContentInput = { + /** Selects the MaxContent branch. */ unit: typeof LengthUnit.MaxContent; +}; +/** Supplies the complete `Dimension.FitContent` input form. */ +type DimensionFitContentInput = { + /** Selects the FitContent branch. */ unit: typeof LengthUnit.FitContent; +}; +/** Supplies the complete `Dimension.FitContentLength(value)` input form. */ +type DimensionFitContentLengthInput = { + /** Selects the FitContentLength branch. */ unit: typeof LengthUnit.FitContentLength; + /** Supplies the numeric payload for `Dimension.FitContentLength(value)`. */ value: number; +}; +/** Supplies the complete `Dimension.FitContentPercent(value)` input form. */ +type DimensionFitContentPercentInput = { + /** Selects the FitContentPercent branch. */ unit: typeof LengthUnit.FitContentPercent; + /** Supplies the numeric payload for `Dimension.FitContentPercent(value)`. */ value: number; +}; +/** Supplies the complete `Dimension.Stretch` input form. */ +type DimensionStretchInput = { + /** Selects the Stretch branch. */ unit: typeof LengthUnit.Stretch; +}; +/** Supplies the complete `Dimension.Content` input form. */ +type DimensionContentInput = { + /** Selects the Content branch. */ unit: typeof LengthUnit.Content; +}; /** Accepts a number as shorthand for `Dimension.Length(value)`, or a complete tagged `LengthPercentageInput` value. */ type LengthPercentageInput = number | LengthInput | PercentInput; /** Represents a complete tagged `LengthPercentage` value returned by the binding. */ @@ -309,9 +357,9 @@ type LengthPercentageAutoInput = number | LengthInput | PercentInput | AutoInput /** Represents a complete tagged `LengthPercentageAuto` value returned by the binding. */ type LengthPercentageAuto = Readonly | Readonly | Readonly; /** Accepts a number as shorthand for `Dimension.Length(value)`, or a complete tagged `DimensionInput` value. */ -type DimensionInput = number | LengthInput | PercentInput | AutoInput; +type DimensionInput = number | LengthInput | PercentInput | AutoInput | DimensionMinContentInput | DimensionMaxContentInput | DimensionFitContentInput | DimensionFitContentLengthInput | DimensionFitContentPercentInput | DimensionStretchInput | DimensionContentInput; /** Represents a complete tagged `Dimension` value returned by the binding. */ -type Dimension = Readonly | Readonly | Readonly; +type Dimension = Readonly | Readonly | Readonly | Readonly | Readonly | Readonly | Readonly | Readonly | Readonly | Readonly; /** Provides complete tagged forms for dimension inputs, including `Dimension.Length(value)`, the form represented by numeric shorthand. */ declare const Dimension: Readonly<{ Length(value: number): LengthInput; @@ -319,6 +367,23 @@ declare const Dimension: Readonly<{ Auto: Readonly<{ readonly unit: 2; }>; + MinContent: Readonly<{ + readonly unit: 3; + }>; + MaxContent: Readonly<{ + readonly unit: 4; + }>; + FitContent: Readonly<{ + readonly unit: 5; + }>; + FitContentLength(value: number): DimensionFitContentLengthInput; + FitContentPercent(value: number): DimensionFitContentPercentInput; + Stretch: Readonly<{ + readonly unit: 8; + }>; + Content: Readonly<{ + readonly unit: 9; + }>; }>; /** Supplies the complete `AvailableSpace.Definite(value)` input form. */ type AvailableSpaceDefiniteInput = { @@ -594,8 +659,8 @@ interface Style { /** Reports the node's stored position style value. */ readonly position: Position; /** Reports the node's stored inset style value. */ readonly inset: Rect; /** Reports the node's stored size style value. */ readonly size: Size; - /** Reports the node's stored min size style value. */ readonly minSize: Size; - /** Reports the node's stored max size style value. */ readonly maxSize: Size; + /** Reports the node's stored min size style value. */ readonly minSize: Size; + /** Reports the node's stored max size style value. */ readonly maxSize: Size; /** Reports the node's stored aspect ratio style value. */ readonly aspectRatio: number | null; /** Reports the node's stored margin style value. */ readonly margin: Rect; /** Reports the node's stored padding style value. */ readonly padding: Rect; @@ -610,6 +675,7 @@ interface Style { /** Reports the node's stored text align style value. */ readonly textAlign: TextAlign; /** Reports the node's stored flex direction style value. */ readonly flexDirection: FlexDirection; /** Reports the node's stored flex wrap style value. */ readonly flexWrap: FlexWrap; + /** Reports the stored unsigned 16-bit minimum flex line count. */ readonly flexLineCount: number; /** Reports the node's stored flex basis style value. */ readonly flexBasis: Dimension; /** Reports the node's stored flex grow style value. */ readonly flexGrow: number; /** Reports the node's stored flex shrink style value. */ readonly flexShrink: number; @@ -781,8 +847,8 @@ interface StyleInput { /** Supplies the node's position style when present. */ position?: Position | undefined; /** Supplies the node's inset style when present. */ inset?: LengthPercentageAutoInput | PartialRectInput | undefined; /** Supplies the node's size style when present. */ size?: DimensionInput | PartialSizeInput | undefined; - /** Supplies the node's min size style when present. */ minSize?: DimensionInput | PartialSizeInput | undefined; - /** Supplies the node's max size style when present. */ maxSize?: DimensionInput | PartialSizeInput | undefined; + /** Supplies the node's min size style when present. */ minSize?: LengthPercentageAutoInput | PartialSizeInput | undefined; + /** Supplies the node's max size style when present. */ maxSize?: LengthPercentageAutoInput | PartialSizeInput | undefined; /** Omission uses a default for replacement and preserves on update; null stores Taffy None. */ aspectRatio?: number | null | undefined; /** Supplies the node's margin style when present. */ margin?: LengthPercentageAutoInput | PartialRectInput | undefined; /** Supplies the node's padding style when present. */ padding?: LengthPercentageInput | PartialRectInput | undefined; @@ -797,6 +863,7 @@ interface StyleInput { /** Supplies the node's text align style when present. */ textAlign?: TextAlign | undefined; /** Supplies the node's flex direction style when present. */ flexDirection?: FlexDirection | undefined; /** Supplies the node's flex wrap style when present. */ flexWrap?: FlexWrap | undefined; + /** Supplies the requested minimum flex line count as an exact integer from 0 through 65535 when present. */ flexLineCount?: number | undefined; /** Supplies the node's flex basis style when present. */ flexBasis?: DimensionInput | undefined; /** Supplies the node's flex grow style when present. */ flexGrow?: number | undefined; /** Supplies the node's flex shrink style when present. */ flexShrink?: number | undefined; diff --git a/packages/taffyjs-node/index.js b/packages/taffyjs-node/index.js index fd49524..fb9e839 100644 --- a/packages/taffyjs-node/index.js +++ b/packages/taffyjs-node/index.js @@ -105,7 +105,11 @@ const FlexWrap = Object.freeze({ /** Selects the Wrap choice from the FlexWrap numeric family. */ Wrap: 1, /** Selects the WrapReverse choice from the FlexWrap numeric family. */ - WrapReverse: 2 + WrapReverse: 2, + /** Selects the Balance choice from the FlexWrap numeric family. */ + Balance: 3, + /** Selects the BalanceReverse choice from the FlexWrap numeric family. */ + BalanceReverse: 4 }); /** Lists the supported grid auto flow choices as stable numeric constants. */ const GridAutoFlow = Object.freeze({ @@ -191,7 +195,21 @@ const LengthUnit = Object.freeze({ /** Selects the Percent choice from the LengthUnit numeric family. */ Percent: 1, /** Selects the Auto choice from the LengthUnit numeric family. */ - Auto: 2 + Auto: 2, + /** Selects the MinContent choice from the LengthUnit numeric family. */ + MinContent: 3, + /** Selects the MaxContent choice from the LengthUnit numeric family. */ + MaxContent: 4, + /** Selects the FitContent choice from the LengthUnit numeric family. */ + FitContent: 5, + /** Selects the FitContentLength choice from the LengthUnit numeric family. */ + FitContentLength: 6, + /** Selects the FitContentPercent choice from the LengthUnit numeric family. */ + FitContentPercent: 7, + /** Selects the Stretch choice from the LengthUnit numeric family. */ + Stretch: 8, + /** Selects the Content choice from the LengthUnit numeric family. */ + Content: 9 }); /** Lists the supported available space kind choices as stable numeric constants. */ const AvailableSpaceKind = Object.freeze({ @@ -387,6 +405,11 @@ const GridTemplateComponent = Object.freeze({ //#endregion //#region src/tagged-values.ts const dimensionAuto = Object.freeze({ unit: LengthUnit.Auto }); +const dimensionMinContent = Object.freeze({ unit: LengthUnit.MinContent }); +const dimensionMaxContent = Object.freeze({ unit: LengthUnit.MaxContent }); +const dimensionFitContent = Object.freeze({ unit: LengthUnit.FitContent }); +const dimensionStretch = Object.freeze({ unit: LengthUnit.Stretch }); +const dimensionContent = Object.freeze({ unit: LengthUnit.Content }); /** Provides complete tagged forms for dimension inputs, including `Dimension.Length(value)`, the form represented by numeric shorthand. */ const Dimension = Object.freeze({ Length(value) { @@ -401,7 +424,24 @@ const Dimension = Object.freeze({ value }; }, - Auto: dimensionAuto + Auto: dimensionAuto, + MinContent: dimensionMinContent, + MaxContent: dimensionMaxContent, + FitContent: dimensionFitContent, + FitContentLength(value) { + return { + unit: LengthUnit.FitContentLength, + value + }; + }, + FitContentPercent(value) { + return { + unit: LengthUnit.FitContentPercent, + value + }; + }, + Stretch: dimensionStretch, + Content: dimensionContent }); const availableSpaceMinContent = Object.freeze({ kind: AvailableSpaceKind.MinContent }); const availableSpaceMaxContent = Object.freeze({ kind: AvailableSpaceKind.MaxContent }); @@ -1173,7 +1213,7 @@ var StyleEncoder = class { partialRectLengthPercentageAuto(value, name) { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, true, name); + this.#length(value, true, false, name); return; } const object = geometryObject(value, RECT_FIELDS, name); @@ -1182,28 +1222,41 @@ var StyleEncoder = class { const top = object.top; const bottom = object.bottom; this.#u8((left === void 0 ? 0 : 1) | (right === void 0 ? 0 : 2) | (top === void 0 ? 0 : 4) | (bottom === void 0 ? 0 : 8)); - if (left !== void 0) this.#length(left, true, `${name}.left`); - if (right !== void 0) this.#length(right, true, `${name}.right`); - if (top !== void 0) this.#length(top, true, `${name}.top`); - if (bottom !== void 0) this.#length(bottom, true, `${name}.bottom`); + if (left !== void 0) this.#length(left, true, false, `${name}.left`); + if (right !== void 0) this.#length(right, true, false, `${name}.right`); + if (top !== void 0) this.#length(top, true, false, `${name}.top`); + if (bottom !== void 0) this.#length(bottom, true, false, `${name}.bottom`); } partialSizeDimension(value, name) { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, true, name); + this.#length(value, true, true, name); return; } const object = geometryObject(value, SIZE_FIELDS, name); const width = object.width; const height = object.height; this.#u8((width === void 0 ? 0 : 1) | (height === void 0 ? 0 : 2)); - if (width !== void 0) this.#length(width, true, `${name}.width`); - if (height !== void 0) this.#length(height, true, `${name}.height`); + if (width !== void 0) this.#length(width, true, true, `${name}.width`); + if (height !== void 0) this.#length(height, true, true, `${name}.height`); + } + partialSizeLengthPercentageAuto(value, name) { + if (this.#isLengthInput(value, name)) { + this.#u8(SCALAR_GEOMETRY); + this.#length(value, true, false, name); + return; + } + const object = geometryObject(value, SIZE_FIELDS, name); + const width = object.width; + const height = object.height; + this.#u8((width === void 0 ? 0 : 1) | (height === void 0 ? 0 : 2)); + if (width !== void 0) this.#length(width, true, false, `${name}.width`); + if (height !== void 0) this.#length(height, true, false, `${name}.height`); } partialRectLengthPercentage(value, name) { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, false, name); + this.#length(value, false, false, name); return; } const object = geometryObject(value, RECT_FIELDS, name); @@ -1212,26 +1265,29 @@ var StyleEncoder = class { const top = object.top; const bottom = object.bottom; this.#u8((left === void 0 ? 0 : 1) | (right === void 0 ? 0 : 2) | (top === void 0 ? 0 : 4) | (bottom === void 0 ? 0 : 8)); - if (left !== void 0) this.#length(left, false, `${name}.left`); - if (right !== void 0) this.#length(right, false, `${name}.right`); - if (top !== void 0) this.#length(top, false, `${name}.top`); - if (bottom !== void 0) this.#length(bottom, false, `${name}.bottom`); + if (left !== void 0) this.#length(left, false, false, `${name}.left`); + if (right !== void 0) this.#length(right, false, false, `${name}.right`); + if (top !== void 0) this.#length(top, false, false, `${name}.top`); + if (bottom !== void 0) this.#length(bottom, false, false, `${name}.bottom`); } partialSizeLengthPercentage(value, name) { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, false, name); + this.#length(value, false, false, name); return; } const object = geometryObject(value, SIZE_FIELDS, name); const width = object.width; const height = object.height; this.#u8((width === void 0 ? 0 : 1) | (height === void 0 ? 0 : 2)); - if (width !== void 0) this.#length(width, false, `${name}.width`); - if (height !== void 0) this.#length(height, false, `${name}.height`); + if (width !== void 0) this.#length(width, false, false, `${name}.width`); + if (height !== void 0) this.#length(height, false, false, `${name}.height`); } dimension(value, name) { - this.#length(value, true, name); + this.#length(value, true, true, name); + } + unsigned16(value, name) { + this.#u16(inputInteger(value, 0, 65535, name)); } gridTemplateComponents(value, name) { const values = inputArray(value, name); @@ -1289,7 +1345,7 @@ var StyleEncoder = class { inputNumber(unit, `${name}.unit`); return true; } - #length(value, allowAuto, name) { + #length(value, allowAuto, allowIntrinsic, name) { if (typeof value === "number") { this.#u8(LengthUnit.Length); this.#f64(value); @@ -1297,10 +1353,10 @@ var StyleEncoder = class { } const object = inputObject(value, name); const unit = inputInteger(object.unit, 0, 255, `${name}.unit`); - if (unit !== LengthUnit.Length && unit !== LengthUnit.Percent && unit !== LengthUnit.Auto) throw rangeError(`${name}.unit`, "a supported length unit"); + if (unit !== LengthUnit.Length && unit !== LengthUnit.Percent && unit !== LengthUnit.Auto && (!allowIntrinsic || unit !== LengthUnit.MinContent && unit !== LengthUnit.MaxContent && unit !== LengthUnit.FitContent && unit !== LengthUnit.FitContentLength && unit !== LengthUnit.FitContentPercent && unit !== LengthUnit.Stretch && unit !== LengthUnit.Content)) throw rangeError(`${name}.unit`, "a supported length unit"); const payload = object.value; if (payload !== void 0) inputNumber(payload, `${name}.value`); - if (unit === LengthUnit.Auto) { + if (unit === LengthUnit.Auto || unit === LengthUnit.MinContent || unit === LengthUnit.MaxContent || unit === LengthUnit.FitContent || unit === LengthUnit.Stretch || unit === LengthUnit.Content) { if (!allowAuto) throw typeError(name, "a non-Auto length"); this.#u8(unit); return; @@ -1343,7 +1399,7 @@ var StyleEncoder = class { if (!maximum && (kind === TrackSizingKind.FitContent || kind === TrackSizingKind.Fr)) throw typeError(name, "a valid minimum track value"); this.#u8(kind); if (kind === TrackSizingKind.Length || kind === TrackSizingKind.Percent || kind === TrackSizingKind.Fr) this.#f64(inputNumber(payload, `${name}.value`)); - else if (kind === TrackSizingKind.FitContent) this.#length(payload, false, `${name}.value`); + else if (kind === TrackSizingKind.FitContent) this.#length(payload, false, false, `${name}.value`); } #gridTemplateComponent(value, name) { const object = inputObject(value, name); @@ -1439,7 +1495,7 @@ var StyleEncoder = class { //#endregion //#region src/style-input.ts function withEncodedStyle(style, use) { - return withStyleEncoder(style, 2, 6, (encoder) => { + return withStyleEncoder(style, 3, 6, (encoder) => { const display = style.display; if (display !== void 0) { encoder.field(0); @@ -1522,13 +1578,13 @@ function withEncodedStyle(style, use) { if (minSize !== void 0) { encoder.field(13); const value = minSize; - encoder.partialSizeDimension(value, "Style.minSize"); + encoder.partialSizeLengthPercentageAuto(value, "Style.minSize"); } const maxSize = style.maxSize; if (maxSize !== void 0) { encoder.field(14); const value = maxSize; - encoder.partialSizeDimension(value, "Style.maxSize"); + encoder.partialSizeLengthPercentageAuto(value, "Style.maxSize"); } const aspectRatio = style.aspectRatio; if (aspectRatio !== void 0) { @@ -1612,83 +1668,89 @@ function withEncodedStyle(style, use) { if (flexWrap !== void 0) { encoder.field(28); const value = flexWrap; - encoder.enumeration(value, 7, "Style.flexWrap"); + encoder.enumeration(value, 31, "Style.flexWrap"); + } + const flexLineCount = style.flexLineCount; + if (flexLineCount !== void 0) { + encoder.field(29); + const value = flexLineCount; + encoder.unsigned16(value, "Style.flexLineCount"); } const flexBasis = style.flexBasis; if (flexBasis !== void 0) { - encoder.field(29); + encoder.field(30); const value = flexBasis; encoder.dimension(value, "Style.flexBasis"); } const flexGrow = style.flexGrow; if (flexGrow !== void 0) { - encoder.field(30); + encoder.field(31); const value = flexGrow; encoder.number(value, "Style.flexGrow"); } const flexShrink = style.flexShrink; if (flexShrink !== void 0) { - encoder.field(31); + encoder.field(32); const value = flexShrink; encoder.number(value, "Style.flexShrink"); } const gridTemplateRows = style.gridTemplateRows; if (gridTemplateRows !== void 0) { - encoder.field(32); + encoder.field(33); const value = gridTemplateRows; encoder.gridTemplateComponents(value, "Style.gridTemplateRows"); } const gridTemplateColumns = style.gridTemplateColumns; if (gridTemplateColumns !== void 0) { - encoder.field(33); + encoder.field(34); const value = gridTemplateColumns; encoder.gridTemplateComponents(value, "Style.gridTemplateColumns"); } const gridAutoRows = style.gridAutoRows; if (gridAutoRows !== void 0) { - encoder.field(34); + encoder.field(35); const value = gridAutoRows; encoder.trackSizingFunctions(value, "Style.gridAutoRows"); } const gridAutoColumns = style.gridAutoColumns; if (gridAutoColumns !== void 0) { - encoder.field(35); + encoder.field(36); const value = gridAutoColumns; encoder.trackSizingFunctions(value, "Style.gridAutoColumns"); } const gridAutoFlow = style.gridAutoFlow; if (gridAutoFlow !== void 0) { - encoder.field(36); + encoder.field(37); const value = gridAutoFlow; encoder.enumeration(value, 15, "Style.gridAutoFlow"); } const gridTemplateAreas = style.gridTemplateAreas; if (gridTemplateAreas !== void 0) { - encoder.field(37); + encoder.field(38); const value = gridTemplateAreas; encoder.nullableGridTemplateAreas(value, "Style.gridTemplateAreas"); } const gridTemplateColumnNames = style.gridTemplateColumnNames; if (gridTemplateColumnNames !== void 0) { - encoder.field(38); + encoder.field(39); const value = gridTemplateColumnNames; encoder.stringMatrix(value, "Style.gridTemplateColumnNames"); } const gridTemplateRowNames = style.gridTemplateRowNames; if (gridTemplateRowNames !== void 0) { - encoder.field(39); + encoder.field(40); const value = gridTemplateRowNames; encoder.stringMatrix(value, "Style.gridTemplateRowNames"); } const gridRow = style.gridRow; if (gridRow !== void 0) { - encoder.field(40); + encoder.field(41); const value = gridRow; encoder.partialLineGridPlacement(value, "Style.gridRow"); } const gridColumn = style.gridColumn; if (gridColumn !== void 0) { - encoder.field(41); + encoder.field(42); const value = gridColumn; encoder.partialLineGridPlacement(value, "Style.gridColumn"); } diff --git a/packages/taffyjs-node/src/numeric-families.ts b/packages/taffyjs-node/src/numeric-families.ts index adbb22b..a5aa8d0 100644 --- a/packages/taffyjs-node/src/numeric-families.ts +++ b/packages/taffyjs-node/src/numeric-families.ts @@ -141,6 +141,10 @@ export const FlexWrap = Object.freeze({ Wrap: 1, /** Selects the WrapReverse choice from the FlexWrap numeric family. */ WrapReverse: 2, + /** Selects the Balance choice from the FlexWrap numeric family. */ + Balance: 3, + /** Selects the BalanceReverse choice from the FlexWrap numeric family. */ + BalanceReverse: 4, } as const); export type FlexWrap = EnumValue; @@ -239,6 +243,20 @@ export const LengthUnit = Object.freeze({ Percent: 1, /** Selects the Auto choice from the LengthUnit numeric family. */ Auto: 2, + /** Selects the MinContent choice from the LengthUnit numeric family. */ + MinContent: 3, + /** Selects the MaxContent choice from the LengthUnit numeric family. */ + MaxContent: 4, + /** Selects the FitContent choice from the LengthUnit numeric family. */ + FitContent: 5, + /** Selects the FitContentLength choice from the LengthUnit numeric family. */ + FitContentLength: 6, + /** Selects the FitContentPercent choice from the LengthUnit numeric family. */ + FitContentPercent: 7, + /** Selects the Stretch choice from the LengthUnit numeric family. */ + Stretch: 8, + /** Selects the Content choice from the LengthUnit numeric family. */ + Content: 9, } as const); export type LengthUnit = EnumValue; diff --git a/packages/taffyjs-node/src/public-types.ts b/packages/taffyjs-node/src/public-types.ts index 0470f4d..f6b88de 100644 --- a/packages/taffyjs-node/src/public-types.ts +++ b/packages/taffyjs-node/src/public-types.ts @@ -340,8 +340,8 @@ export interface Style { /** Reports the node's stored position style value. */ readonly position: Position; /** Reports the node's stored inset style value. */ readonly inset: Rect; /** Reports the node's stored size style value. */ readonly size: Size; - /** Reports the node's stored min size style value. */ readonly minSize: Size; - /** Reports the node's stored max size style value. */ readonly maxSize: Size; + /** Reports the node's stored min size style value. */ readonly minSize: Size; + /** Reports the node's stored max size style value. */ readonly maxSize: Size; /** Reports the node's stored aspect ratio style value. */ readonly aspectRatio: number | null; /** Reports the node's stored margin style value. */ readonly margin: Rect; /** Reports the node's stored padding style value. */ readonly padding: Rect; @@ -356,6 +356,7 @@ export interface Style { /** Reports the node's stored text align style value. */ readonly textAlign: TextAlign; /** Reports the node's stored flex direction style value. */ readonly flexDirection: FlexDirection; /** Reports the node's stored flex wrap style value. */ readonly flexWrap: FlexWrap; + /** Reports the stored unsigned 16-bit minimum flex line count. */ readonly flexLineCount: number; /** Reports the node's stored flex basis style value. */ readonly flexBasis: Dimension; /** Reports the node's stored flex grow style value. */ readonly flexGrow: number; /** Reports the node's stored flex shrink style value. */ readonly flexShrink: number; diff --git a/packages/taffyjs-node/src/style-codec.ts b/packages/taffyjs-node/src/style-codec.ts index a91bc13..863cf47 100644 --- a/packages/taffyjs-node/src/style-codec.ts +++ b/packages/taffyjs-node/src/style-codec.ts @@ -187,7 +187,7 @@ export class StyleEncoder { partialRectLengthPercentageAuto(value: unknown, name: string): void { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, true, name); + this.#length(value, true, false, name); return; } const object = geometryObject(value, RECT_FIELDS, name); @@ -201,30 +201,44 @@ export class StyleEncoder { (top === undefined ? 0 : 4) | (bottom === undefined ? 0 : 8), ); - if (left !== undefined) this.#length(left, true, `${name}.left`); - if (right !== undefined) this.#length(right, true, `${name}.right`); - if (top !== undefined) this.#length(top, true, `${name}.top`); - if (bottom !== undefined) this.#length(bottom, true, `${name}.bottom`); + if (left !== undefined) this.#length(left, true, false, `${name}.left`); + if (right !== undefined) this.#length(right, true, false, `${name}.right`); + if (top !== undefined) this.#length(top, true, false, `${name}.top`); + if (bottom !== undefined) this.#length(bottom, true, false, `${name}.bottom`); } partialSizeDimension(value: unknown, name: string): void { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, true, name); + this.#length(value, true, true, name); return; } const object = geometryObject(value, SIZE_FIELDS, name); const width = object.width; const height = object.height; this.#u8((width === undefined ? 0 : 1) | (height === undefined ? 0 : 2)); - if (width !== undefined) this.#length(width, true, `${name}.width`); - if (height !== undefined) this.#length(height, true, `${name}.height`); + if (width !== undefined) this.#length(width, true, true, `${name}.width`); + if (height !== undefined) this.#length(height, true, true, `${name}.height`); + } + + partialSizeLengthPercentageAuto(value: unknown, name: string): void { + if (this.#isLengthInput(value, name)) { + this.#u8(SCALAR_GEOMETRY); + this.#length(value, true, false, name); + return; + } + const object = geometryObject(value, SIZE_FIELDS, name); + const width = object.width; + const height = object.height; + this.#u8((width === undefined ? 0 : 1) | (height === undefined ? 0 : 2)); + if (width !== undefined) this.#length(width, true, false, `${name}.width`); + if (height !== undefined) this.#length(height, true, false, `${name}.height`); } partialRectLengthPercentage(value: unknown, name: string): void { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, false, name); + this.#length(value, false, false, name); return; } const object = geometryObject(value, RECT_FIELDS, name); @@ -238,28 +252,32 @@ export class StyleEncoder { (top === undefined ? 0 : 4) | (bottom === undefined ? 0 : 8), ); - if (left !== undefined) this.#length(left, false, `${name}.left`); - if (right !== undefined) this.#length(right, false, `${name}.right`); - if (top !== undefined) this.#length(top, false, `${name}.top`); - if (bottom !== undefined) this.#length(bottom, false, `${name}.bottom`); + if (left !== undefined) this.#length(left, false, false, `${name}.left`); + if (right !== undefined) this.#length(right, false, false, `${name}.right`); + if (top !== undefined) this.#length(top, false, false, `${name}.top`); + if (bottom !== undefined) this.#length(bottom, false, false, `${name}.bottom`); } partialSizeLengthPercentage(value: unknown, name: string): void { if (this.#isLengthInput(value, name)) { this.#u8(SCALAR_GEOMETRY); - this.#length(value, false, name); + this.#length(value, false, false, name); return; } const object = geometryObject(value, SIZE_FIELDS, name); const width = object.width; const height = object.height; this.#u8((width === undefined ? 0 : 1) | (height === undefined ? 0 : 2)); - if (width !== undefined) this.#length(width, false, `${name}.width`); - if (height !== undefined) this.#length(height, false, `${name}.height`); + if (width !== undefined) this.#length(width, false, false, `${name}.width`); + if (height !== undefined) this.#length(height, false, false, `${name}.height`); } dimension(value: unknown, name: string): void { - this.#length(value, true, name); + this.#length(value, true, true, name); + } + + unsigned16(value: unknown, name: string): void { + this.#u16(inputInteger(value, 0, 0xffff, name)); } gridTemplateComponents(value: unknown, name: string): void { @@ -329,7 +347,7 @@ export class StyleEncoder { return true; } - #length(value: unknown, allowAuto: boolean, name: string): void { + #length(value: unknown, allowAuto: boolean, allowIntrinsic: boolean, name: string): void { if (typeof value === "number") { this.#u8(LengthUnit.Length); this.#f64(value); @@ -337,12 +355,31 @@ export class StyleEncoder { } const object = inputObject(value, name); const unit = inputInteger(object.unit, 0, 0xff, `${name}.unit`); - if (unit !== LengthUnit.Length && unit !== LengthUnit.Percent && unit !== LengthUnit.Auto) { + if ( + unit !== LengthUnit.Length && + unit !== LengthUnit.Percent && + unit !== LengthUnit.Auto && + (!allowIntrinsic || + (unit !== LengthUnit.MinContent && + unit !== LengthUnit.MaxContent && + unit !== LengthUnit.FitContent && + unit !== LengthUnit.FitContentLength && + unit !== LengthUnit.FitContentPercent && + unit !== LengthUnit.Stretch && + unit !== LengthUnit.Content)) + ) { throw rangeError(`${name}.unit`, "a supported length unit"); } const payload = object.value; if (payload !== undefined) inputNumber(payload, `${name}.value`); - if (unit === LengthUnit.Auto) { + if ( + unit === LengthUnit.Auto || + unit === LengthUnit.MinContent || + unit === LengthUnit.MaxContent || + unit === LengthUnit.FitContent || + unit === LengthUnit.Stretch || + unit === LengthUnit.Content + ) { if (!allowAuto) throw typeError(name, "a non-Auto length"); this.#u8(unit); return; @@ -416,7 +453,7 @@ export class StyleEncoder { ) { this.#f64(inputNumber(payload, `${name}.value`)); } else if (kind === TrackSizingKind.FitContent) { - this.#length(payload, false, `${name}.value`); + this.#length(payload, false, false, `${name}.value`); } } diff --git a/packages/taffyjs-node/src/style-input.ts b/packages/taffyjs-node/src/style-input.ts index c2a6f47..fb7c965 100644 --- a/packages/taffyjs-node/src/style-input.ts +++ b/packages/taffyjs-node/src/style-input.ts @@ -65,12 +65,12 @@ export interface StyleInput { | PartialSizeInput | undefined; /** Supplies the node's min size style when present. */ minSize?: - | DimensionInput - | PartialSizeInput + | LengthPercentageAutoInput + | PartialSizeInput | undefined; /** Supplies the node's max size style when present. */ maxSize?: - | DimensionInput - | PartialSizeInput + | LengthPercentageAutoInput + | PartialSizeInput | undefined; /** Omission uses a default for replacement and preserves on update; null stores Taffy None. */ aspectRatio?: | number @@ -121,6 +121,9 @@ export interface StyleInput { | FlexDirection | undefined; /** Supplies the node's flex wrap style when present. */ flexWrap?: FlexWrap | undefined; + /** Supplies the requested minimum flex line count as an exact integer from 0 through 65535 when present. */ flexLineCount?: + | number + | undefined; /** Supplies the node's flex basis style when present. */ flexBasis?: DimensionInput | undefined; /** Supplies the node's flex grow style when present. */ flexGrow?: number | undefined; /** Supplies the node's flex shrink style when present. */ flexShrink?: number | undefined; @@ -161,7 +164,7 @@ export interface StyleInput { export type StyleUpdate = StyleInput; export function withEncodedStyle(style: StyleInput, use: (encoded: Uint8Array) => T): T { - return withStyleEncoder(style, 2, 6, (encoder) => { + return withStyleEncoder(style, 3, 6, (encoder) => { const display = style.display; if (display !== undefined) { encoder.field(0); @@ -244,13 +247,13 @@ export function withEncodedStyle(style: StyleInput, use: (encoded: Uint8Array if (minSize !== undefined) { encoder.field(13); const value = minSize; - encoder.partialSizeDimension(value, "Style.minSize"); + encoder.partialSizeLengthPercentageAuto(value, "Style.minSize"); } const maxSize = style.maxSize; if (maxSize !== undefined) { encoder.field(14); const value = maxSize; - encoder.partialSizeDimension(value, "Style.maxSize"); + encoder.partialSizeLengthPercentageAuto(value, "Style.maxSize"); } const aspectRatio = style.aspectRatio; if (aspectRatio !== undefined) { @@ -334,83 +337,89 @@ export function withEncodedStyle(style: StyleInput, use: (encoded: Uint8Array if (flexWrap !== undefined) { encoder.field(28); const value = flexWrap; - encoder.enumeration(value, 7, "Style.flexWrap"); + encoder.enumeration(value, 31, "Style.flexWrap"); + } + const flexLineCount = style.flexLineCount; + if (flexLineCount !== undefined) { + encoder.field(29); + const value = flexLineCount; + encoder.unsigned16(value, "Style.flexLineCount"); } const flexBasis = style.flexBasis; if (flexBasis !== undefined) { - encoder.field(29); + encoder.field(30); const value = flexBasis; encoder.dimension(value, "Style.flexBasis"); } const flexGrow = style.flexGrow; if (flexGrow !== undefined) { - encoder.field(30); + encoder.field(31); const value = flexGrow; encoder.number(value, "Style.flexGrow"); } const flexShrink = style.flexShrink; if (flexShrink !== undefined) { - encoder.field(31); + encoder.field(32); const value = flexShrink; encoder.number(value, "Style.flexShrink"); } const gridTemplateRows = style.gridTemplateRows; if (gridTemplateRows !== undefined) { - encoder.field(32); + encoder.field(33); const value = gridTemplateRows; encoder.gridTemplateComponents(value, "Style.gridTemplateRows"); } const gridTemplateColumns = style.gridTemplateColumns; if (gridTemplateColumns !== undefined) { - encoder.field(33); + encoder.field(34); const value = gridTemplateColumns; encoder.gridTemplateComponents(value, "Style.gridTemplateColumns"); } const gridAutoRows = style.gridAutoRows; if (gridAutoRows !== undefined) { - encoder.field(34); + encoder.field(35); const value = gridAutoRows; encoder.trackSizingFunctions(value, "Style.gridAutoRows"); } const gridAutoColumns = style.gridAutoColumns; if (gridAutoColumns !== undefined) { - encoder.field(35); + encoder.field(36); const value = gridAutoColumns; encoder.trackSizingFunctions(value, "Style.gridAutoColumns"); } const gridAutoFlow = style.gridAutoFlow; if (gridAutoFlow !== undefined) { - encoder.field(36); + encoder.field(37); const value = gridAutoFlow; encoder.enumeration(value, 15, "Style.gridAutoFlow"); } const gridTemplateAreas = style.gridTemplateAreas; if (gridTemplateAreas !== undefined) { - encoder.field(37); + encoder.field(38); const value = gridTemplateAreas; encoder.nullableGridTemplateAreas(value, "Style.gridTemplateAreas"); } const gridTemplateColumnNames = style.gridTemplateColumnNames; if (gridTemplateColumnNames !== undefined) { - encoder.field(38); + encoder.field(39); const value = gridTemplateColumnNames; encoder.stringMatrix(value, "Style.gridTemplateColumnNames"); } const gridTemplateRowNames = style.gridTemplateRowNames; if (gridTemplateRowNames !== undefined) { - encoder.field(39); + encoder.field(40); const value = gridTemplateRowNames; encoder.stringMatrix(value, "Style.gridTemplateRowNames"); } const gridRow = style.gridRow; if (gridRow !== undefined) { - encoder.field(40); + encoder.field(41); const value = gridRow; encoder.partialLineGridPlacement(value, "Style.gridRow"); } const gridColumn = style.gridColumn; if (gridColumn !== undefined) { - encoder.field(41); + encoder.field(42); const value = gridColumn; encoder.partialLineGridPlacement(value, "Style.gridColumn"); } diff --git a/packages/taffyjs-node/src/tagged-values.ts b/packages/taffyjs-node/src/tagged-values.ts index 11cfc4b..0a7794d 100644 --- a/packages/taffyjs-node/src/tagged-values.ts +++ b/packages/taffyjs-node/src/tagged-values.ts @@ -21,6 +21,43 @@ export type AutoInput = { /** Selects the Auto branch. */ unit: typeof LengthUnit.Auto; }; +/** Supplies the complete `Dimension.MinContent` input form. */ +type DimensionMinContentInput = { + /** Selects the MinContent branch. */ unit: typeof LengthUnit.MinContent; +}; + +/** Supplies the complete `Dimension.MaxContent` input form. */ +type DimensionMaxContentInput = { + /** Selects the MaxContent branch. */ unit: typeof LengthUnit.MaxContent; +}; + +/** Supplies the complete `Dimension.FitContent` input form. */ +type DimensionFitContentInput = { + /** Selects the FitContent branch. */ unit: typeof LengthUnit.FitContent; +}; + +/** Supplies the complete `Dimension.FitContentLength(value)` input form. */ +type DimensionFitContentLengthInput = { + /** Selects the FitContentLength branch. */ unit: typeof LengthUnit.FitContentLength; + /** Supplies the numeric payload for `Dimension.FitContentLength(value)`. */ value: number; +}; + +/** Supplies the complete `Dimension.FitContentPercent(value)` input form. */ +type DimensionFitContentPercentInput = { + /** Selects the FitContentPercent branch. */ unit: typeof LengthUnit.FitContentPercent; + /** Supplies the numeric payload for `Dimension.FitContentPercent(value)`. */ value: number; +}; + +/** Supplies the complete `Dimension.Stretch` input form. */ +type DimensionStretchInput = { + /** Selects the Stretch branch. */ unit: typeof LengthUnit.Stretch; +}; + +/** Supplies the complete `Dimension.Content` input form. */ +type DimensionContentInput = { + /** Selects the Content branch. */ unit: typeof LengthUnit.Content; +}; + /** Accepts a number as shorthand for `Dimension.Length(value)`, or a complete tagged `LengthPercentageInput` value. */ export type LengthPercentageInput = number | LengthInput | PercentInput; @@ -37,13 +74,44 @@ export type LengthPercentageAuto = | Readonly; /** Accepts a number as shorthand for `Dimension.Length(value)`, or a complete tagged `DimensionInput` value. */ -export type DimensionInput = number | LengthInput | PercentInput | AutoInput; +export type DimensionInput = + | number + | LengthInput + | PercentInput + | AutoInput + | DimensionMinContentInput + | DimensionMaxContentInput + | DimensionFitContentInput + | DimensionFitContentLengthInput + | DimensionFitContentPercentInput + | DimensionStretchInput + | DimensionContentInput; /** Represents a complete tagged `Dimension` value returned by the binding. */ -export type Dimension = Readonly | Readonly | Readonly; +export type Dimension = + | Readonly + | Readonly + | Readonly + | Readonly + | Readonly + | Readonly + | Readonly + | Readonly + | Readonly + | Readonly; const dimensionAuto = Object.freeze({ unit: LengthUnit.Auto } as const); +const dimensionMinContent = Object.freeze({ unit: LengthUnit.MinContent } as const); + +const dimensionMaxContent = Object.freeze({ unit: LengthUnit.MaxContent } as const); + +const dimensionFitContent = Object.freeze({ unit: LengthUnit.FitContent } as const); + +const dimensionStretch = Object.freeze({ unit: LengthUnit.Stretch } as const); + +const dimensionContent = Object.freeze({ unit: LengthUnit.Content } as const); + /** Provides complete tagged forms for dimension inputs, including `Dimension.Length(value)`, the form represented by numeric shorthand. */ export const Dimension = Object.freeze({ Length(value: number): LengthInput { @@ -53,6 +121,17 @@ export const Dimension = Object.freeze({ return { unit: LengthUnit.Percent, value }; }, Auto: dimensionAuto, + MinContent: dimensionMinContent, + MaxContent: dimensionMaxContent, + FitContent: dimensionFitContent, + FitContentLength(value: number): DimensionFitContentLengthInput { + return { unit: LengthUnit.FitContentLength, value }; + }, + FitContentPercent(value: number): DimensionFitContentPercentInput { + return { unit: LengthUnit.FitContentPercent, value }; + }, + Stretch: dimensionStretch, + Content: dimensionContent, }); /** Supplies the complete `AvailableSpace.Definite(value)` input form. */ diff --git a/packages/taffyjs-yoga/src/translate.ts b/packages/taffyjs-yoga/src/translate.ts index 2c2d9e5..1cd7cee 100644 --- a/packages/taffyjs-yoga/src/translate.ts +++ b/packages/taffyjs-yoga/src/translate.ts @@ -11,7 +11,7 @@ import { Position as TaffyPosition, type AlignContent as TaffyAlignContentValue, type AlignItems as TaffyAlignItemsValue, - type DimensionInput, + type LengthPercentageAutoInput, type StyleInput, } from "@taffyjs/node"; import { resolveEdge, resolveGutter, type YogaDeclarations } from "./declarations.js"; @@ -169,7 +169,7 @@ function rect(read: (edge: Edge.Left | Edge.Top | Edge.Right | Edge.Bottom) = function effectiveFlexBasis( declarations: YogaDeclarations, config: TranslationConfig, -): DimensionInput { +): LengthPercentageAutoInput { if ( declarations.flexBasis.unit === Unit.Auto && declarations.flex !== undefined && @@ -356,7 +356,7 @@ function rootMinimumOverride( minimum: YogaValue, maximum: YogaValue, ownerSize: number | undefined, -): DimensionInput | undefined { +): LengthPercentageAutoInput | undefined { const declaredLength = resolveLength(declared, ownerSize); const minimumLength = resolveLength(minimum, ownerSize); const maximumLength = resolveLength(maximum, ownerSize); diff --git a/packages/taffyjs-yoga/src/values.ts b/packages/taffyjs-yoga/src/values.ts index 7fce95a..6929606 100644 --- a/packages/taffyjs-yoga/src/values.ts +++ b/packages/taffyjs-yoga/src/values.ts @@ -1,6 +1,5 @@ import { Dimension as TaffyDimension, - type DimensionInput, type LengthPercentageAutoInput, type LengthPercentageInput, } from "@taffyjs/node"; @@ -122,7 +121,7 @@ export function normalizeAspectRatio(value: number | undefined): number | undefi return normalized === undefined || normalized === 0 ? undefined : normalized; } -export function toDimension(value: YogaValue): DimensionInput { +export function toDimension(value: YogaValue): LengthPercentageAutoInput { switch (value.unit) { case Unit.Point: return TaffyDimension.Length(value.value); @@ -134,14 +133,14 @@ export function toDimension(value: YogaValue): DimensionInput { } } -export function toSizeDimension(value: YogaValue): DimensionInput { +export function toSizeDimension(value: YogaValue): LengthPercentageAutoInput { if ((value.unit === Unit.Point || value.unit === Unit.Percent) && value.value < 0) { return TaffyDimension.Auto; } return toDimension(value); } -export function toMinDimension(value: YogaValue): DimensionInput { +export function toMinDimension(value: YogaValue): LengthPercentageAutoInput { return value.unit === Unit.Undefined ? TaffyDimension.Length(0) : toDimension(value); } diff --git a/tests/taffyjs-node/tests/layouts/algorithms.test.mts b/tests/taffyjs-node/tests/layouts/algorithms.test.mts index 634e2b4..cd59f32 100644 --- a/tests/taffyjs-node/tests/layouts/algorithms.test.mts +++ b/tests/taffyjs-node/tests/layouts/algorithms.test.mts @@ -2,10 +2,12 @@ import assert from "node:assert/strict"; import { AlignItems, AvailableSpace, + AvailableSpaceKind, Clear, Dimension, DetailedLayoutInfoKind, Display, + FlexWrap, Float, GridPlacement, Position, @@ -14,6 +16,7 @@ import { TrackSizingFunction, GridTemplateComponent, Overflow, + type MeasureFunction, type StyleInput, } from "@taffyjs/node"; import { test } from "vite-plus/test"; @@ -151,6 +154,97 @@ test("flex", () => { assert.deepEqual(tree.getUnroundedLayout(second).location, { x: 50.5, y: 0 }); }); +test("balanced flex wrapping honors flexLineCount", () => { + const tree = new TaffyTree(); + tree.disableRounding(); + const children = [31, 32, 33, 34].map((width) => tree.newLeaf({ size: { width, height: 30 } })); + const root = tree.newWithChildren(children, { + display: Display.Flex, + flexWrap: FlexWrap.Balance, + flexLineCount: 3, + size: { width: 100 }, + }); + + tree.computeLayout({ root, availableSpace: maxContentSpace() }); + + assert.deepEqual(tree.getUnroundedLayout(root).size, { width: 100, height: 90 }); + assert.deepEqual( + children.map((child) => tree.getUnroundedLayout(child).location), + [ + { x: 0, y: 0 }, + { x: 31, y: 0 }, + { x: 0, y: 30 }, + { x: 0, y: 60 }, + ], + ); +}); + +test("intrinsic Dimension values drive block sizing", () => { + const tree = new TaffyTree(); + tree.disableRounding(); + const children = [ + Dimension.Stretch, + Dimension.FitContent, + Dimension.MinContent, + Dimension.MaxContent, + Dimension.FitContentLength(30), + Dimension.FitContentPercent(25), + Dimension.Content, + ].map((width) => tree.newLeaf({ size: { width } })); + const root = tree.newWithChildren(children, { + display: Display.Block, + size: { width: 120 }, + }); + const measure: MeasureFunction = ({ knownDimensions, availableSpace }) => { + let intrinsicWidth: number; + switch (availableSpace.width.kind) { + case AvailableSpaceKind.MinContent: + intrinsicWidth = 20; + break; + case AvailableSpaceKind.MaxContent: + intrinsicWidth = 40; + break; + case AvailableSpaceKind.Definite: + intrinsicWidth = Math.max(20, Math.min(availableSpace.width.value, 40)); + break; + } + const width = knownDimensions.width ?? intrinsicWidth; + return { + width, + height: knownDimensions.height ?? (width < 40 ? 20 : 10), + }; + }; + + tree.computeLayout({ root, availableSpace: maxContentSpace(), measure }); + + assert.deepEqual( + children.map((child) => tree.getUnroundedLayout(child).size.width), + [120, 40, 20, 40, 30, 30, 120], + ); +}); + +test("Dimension.Content uses content as the flex basis", () => { + const tree = new TaffyTree(); + tree.disableRounding(); + const child = tree.newLeaf({ + size: { width: 10 }, + flexBasis: Dimension.Content, + flexShrink: 0, + }); + const root = tree.newWithChildren([child], { + display: Display.Flex, + size: { width: 100 }, + }); + + tree.computeLayout({ + root, + availableSpace: maxContentSpace(), + measure: () => ({ width: 40, height: 10 }), + }); + + assert.equal(tree.getUnroundedLayout(child).size.width, 40); +}); + test("auto-margin-alignment", () => { const baseline = new TaffyTree(); baseline.disableRounding(); diff --git a/tests/taffyjs-node/tests/tree/get-style.test.mts b/tests/taffyjs-node/tests/tree/get-style.test.mts index a928139..999b5d2 100644 --- a/tests/taffyjs-node/tests/tree/get-style.test.mts +++ b/tests/taffyjs-node/tests/tree/get-style.test.mts @@ -32,6 +32,7 @@ const STYLE_FIELDS = [ "textAlign", "flexDirection", "flexWrap", + "flexLineCount", "flexBasis", "flexGrow", "flexShrink", diff --git a/tests/taffyjs-node/tests/tree/update-style.test.mts b/tests/taffyjs-node/tests/tree/update-style.test.mts index aab72e0..1955ae1 100644 --- a/tests/taffyjs-node/tests/tree/update-style.test.mts +++ b/tests/taffyjs-node/tests/tree/update-style.test.mts @@ -66,8 +66,9 @@ const fieldCases = [ ["gap", { width: 1, height: 2 }, { width: 3, height: 4 }], ["textAlign", TextAlign.LegacyLeft, TextAlign.LegacyRight], ["flexDirection", FlexDirection.Column, FlexDirection.RowReverse], - ["flexWrap", FlexWrap.Wrap, FlexWrap.WrapReverse], - ["flexBasis", 2, 3], + ["flexWrap", FlexWrap.Balance, FlexWrap.BalanceReverse], + ["flexLineCount", 2, 3], + ["flexBasis", Dimension.MinContent, Dimension.Content], ["flexGrow", 2, 3], ["flexShrink", 2, 3], [ @@ -115,7 +116,7 @@ const allStyleUpdateFieldsCovered: [MissingStyleUpdateField] extends [never] ? t test("applies every top-level field and skips equal or undefined values", () => { assert.equal(allStyleUpdateFieldsCovered, true); - assert.equal(fieldCases.length, 42); + assert.equal(fieldCases.length, 43); for (const [field, initial, next] of fieldCases) { const tree = new TaffyTree(); diff --git a/tests/taffyjs-node/tests/types/style.test-d.ts b/tests/taffyjs-node/tests/types/style.test-d.ts index c4f1a8c..b24411f 100644 --- a/tests/taffyjs-node/tests/types/style.test-d.ts +++ b/tests/taffyjs-node/tests/types/style.test-d.ts @@ -2,6 +2,7 @@ import { AlignItems, Dimension, Display, + FlexWrap, GridPlacement, GridTemplateComponent, RepetitionCount, @@ -17,7 +18,8 @@ const track = TrackSizingFunction.Fr(2); const mutableRows = [GridTemplateComponent.Single(track)]; const input: StyleInput = { display: Display.Grid, - size: { width: 100, height: Dimension.Percent(50) }, + size: { width: Dimension.MinContent, height: Dimension.FitContentPercent(50) }, + minSize: { width: Dimension.Auto }, margin: Dimension.Auto, alignItems: AlignItems.Center, gridTemplateRows: mutableRows, @@ -26,6 +28,9 @@ const input: StyleInput = { ], gridRow: { start: GridPlacement.Line(1), end: GridPlacement.Span(2) }, aspectRatio: null, + flexWrap: FlexWrap.Balance, + flexLineCount: 2, + flexBasis: Dimension.Content, }; input.flexGrow = 1; mutableRows.push(GridTemplateComponent.Single(TrackSizingFunction.Auto)); @@ -58,6 +63,10 @@ tree.newLeaf({ unknownField: true }); tree.newLeaf({ display: 99 }); // @ts-expect-error A numeric length needs its value. tree.newLeaf({ flexBasis: { unit: 0 } }); +// @ts-expect-error Intrinsic Dimension keywords are not valid for minSize. +tree.newLeaf({ minSize: { width: Dimension.MinContent } }); +// @ts-expect-error Intrinsic Dimension keywords are not valid for maxSize. +tree.newLeaf({ maxSize: Dimension.FitContentLength(20) }); // @ts-expect-error Non-nullable Style fields reject null. tree.newLeaf({ padding: null }); // @ts-expect-error Nullable Style output fields are never undefined. diff --git a/tests/taffyjs-node/tests/types/values.test-d.ts b/tests/taffyjs-node/tests/types/values.test-d.ts index b64ec28..e97a057 100644 --- a/tests/taffyjs-node/tests/types/values.test-d.ts +++ b/tests/taffyjs-node/tests/types/values.test-d.ts @@ -69,12 +69,18 @@ if (typeof available !== "number") { declare const dimension: DimensionInput; if (typeof dimension !== "number") { - if (dimension.unit !== LengthUnit.Auto) { - const value: number = dimension.value; - void value; - } else { - // @ts-expect-error Auto has no value payload. - void dimension.value; + switch (dimension.unit) { + case LengthUnit.Length: + case LengthUnit.Percent: + case LengthUnit.FitContentLength: + case LengthUnit.FitContentPercent: { + const value: number = dimension.value; + void value; + break; + } + default: + // @ts-expect-error Fieldless Dimension variants have no value payload. + void dimension.value; } } diff --git a/tests/taffyjs-node/tests/values/style-fields.test.mts b/tests/taffyjs-node/tests/values/style-fields.test.mts index c096919..90c6e43 100644 --- a/tests/taffyjs-node/tests/values/style-fields.test.mts +++ b/tests/taffyjs-node/tests/values/style-fields.test.mts @@ -77,6 +77,7 @@ test("default Style contains every public field and value", () => { textAlign: 0, flexDirection: 0, flexWrap: 0, + flexLineCount: 1, flexBasis: auto(), flexGrow: 0, flexShrink: 1, diff --git a/tests/taffyjs-node/tests/values/style-length.test.mts b/tests/taffyjs-node/tests/values/style-length.test.mts index 2e361e0..f166624 100644 --- a/tests/taffyjs-node/tests/values/style-length.test.mts +++ b/tests/taffyjs-node/tests/values/style-length.test.mts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { Dimension, TaffyTree } from "@taffyjs/node"; +import { Dimension, FlexWrap, LengthUnit, TaffyTree } from "@taffyjs/node"; import { test } from "vite-plus/test"; function createOwner() { @@ -23,6 +23,13 @@ test("Dimension helpers and direct tagged records store the same values", () => [Dimension.Length(12), { unit: 0, value: 12 }], [Dimension.Percent(50), { unit: 1, value: 50 }], [Dimension.Auto, { unit: 2 }], + [Dimension.MinContent, { unit: 3 }], + [Dimension.MaxContent, { unit: 4 }], + [Dimension.FitContent, { unit: 5 }], + [Dimension.FitContentLength(36), { unit: 6, value: 36 }], + [Dimension.FitContentPercent(25), { unit: 7, value: 25 }], + [Dimension.Stretch, { unit: 8 }], + [Dimension.Content, { unit: 9 }], ] as const) { assert.deepEqual( storedStyle({ flexBasis: helper }).flexBasis, @@ -48,6 +55,11 @@ test("percent values use user-facing magnitudes and stored f32 precision", () => const result = storedStyle({ flexBasis: { unit: 1, value } }).flexBasis as { value: number }; assert.equal(result.value, Math.fround(value / 100) * 100); assert.equal(Number.isFinite(result.value), true); + + const fitContent = storedStyle({ + flexBasis: Dimension.FitContentPercent(33.3), + }).flexBasis as { value: number }; + assert.equal(fitContent.value, Math.fround(33.3 / 100) * 100); }); test("length payloads preserve the shared f32 special-value behavior", () => { @@ -61,6 +73,19 @@ test("length payloads preserve the shared f32 special-value behavior", () => { value: number; }; assert.ok(Object.is(percent.value, Math.fround(value / 100) * 100), `percent ${value}`); + + const fitContentLength = storedStyle({ + flexBasis: Dimension.FitContentLength(value), + }).flexBasis as { value: number }; + assert.ok(Object.is(fitContentLength.value, Math.fround(value)), `fit-content length ${value}`); + + const fitContentPercent = storedStyle({ + flexBasis: Dimension.FitContentPercent(value), + }).flexBasis as { value: number }; + assert.ok( + Object.is(fitContentPercent.value, Math.fround(value / 100) * 100), + `fit-content percent ${value}`, + ); } }); @@ -68,9 +93,43 @@ test("semantic lengths reject unsupported shapes and unit values", () => { for (const value of ["auto", true, null, {}, [], { unit: 0 }, { unit: 1 }]) { rejectsWithoutNode({ flexBasis: value }, TypeError); } - for (const value of [{ unit: -1 }, { unit: 3 }, { unit: 0.5 }, { unit: NaN }]) { + for (const value of [{ unit: -1 }, { unit: 10 }, { unit: 0.5 }, { unit: Number.NaN }]) { rejectsWithoutNode({ flexBasis: value }, RangeError); } + for (const unit of [LengthUnit.FitContentLength, LengthUnit.FitContentPercent]) { + rejectsWithoutNode({ flexBasis: { unit } }, TypeError); + } +}); + +test("intrinsic Dimension values stay limited to size and flexBasis", () => { + for (const value of [ + Dimension.MinContent, + Dimension.MaxContent, + Dimension.FitContent, + Dimension.FitContentLength(40), + Dimension.FitContentPercent(50), + Dimension.Stretch, + Dimension.Content, + ]) { + assert.deepEqual(storedStyle({ size: value }).size, { width: value, height: value }); + rejectsWithoutNode({ minSize: value }, RangeError); + rejectsWithoutNode({ maxSize: { width: value } }, RangeError); + } +}); + +test("balanced flex values and the u16 line count round-trip", () => { + for (const flexWrap of [FlexWrap.Balance, FlexWrap.BalanceReverse]) { + const style = storedStyle({ flexWrap, flexLineCount: 3 }); + assert.equal(style.flexWrap, flexWrap); + assert.equal(style.flexLineCount, 3); + } + + assert.equal(storedStyle({ flexLineCount: 0 }).flexLineCount, 0); + assert.equal(storedStyle({ flexLineCount: 0xffff }).flexLineCount, 0xffff); + for (const flexLineCount of [-1, 0x1_0000, 1.5, Number.NaN]) { + rejectsWithoutNode({ flexLineCount }, RangeError); + } + rejectsWithoutNode({ flexLineCount: "2" }, TypeError); }); test("Auto ignores fields from payload-carrying variants", () => { diff --git a/tools/api-codegen/src/emit/style-codec/rust.ts b/tools/api-codegen/src/emit/style-codec/rust.ts index c98d967..6f77e39 100644 --- a/tools/api-codegen/src/emit/style-codec/rust.ts +++ b/tools/api-codegen/src/emit/style-codec/rust.ts @@ -52,10 +52,14 @@ function fieldUpdate(field: StyleField): readonly string[] { return [`changed |= decoder.length_percentage_auto_rect(&mut ${target}, ${name})?;`]; case "partial-size-dimension": return [`changed |= decoder.dimension_size(&mut ${target}, ${name})?;`]; + case "partial-size-length-percentage-auto": + return [`changed |= decoder.length_percentage_auto_size(&mut ${target}, ${name})?;`]; case "partial-rect-length-percentage": return [`changed |= decoder.length_percentage_rect(&mut ${target}, ${name})?;`]; case "partial-size-length-percentage": return [`changed |= decoder.length_percentage_size(&mut ${target}, ${name})?;`]; + case "unsigned-16": + return replacement("replace", target, `decoder.unsigned_16(${name})?`, true); case "dimension": return replacement("replace", target, `decoder.dimension(${name})?`, true); case "grid-template-component-array": diff --git a/tools/api-codegen/src/emit/style-codec/typescript.ts b/tools/api-codegen/src/emit/style-codec/typescript.ts index f7619d5..177c566 100644 --- a/tools/api-codegen/src/emit/style-codec/typescript.ts +++ b/tools/api-codegen/src/emit/style-codec/typescript.ts @@ -8,10 +8,13 @@ const publicTypeByCategory = { "partial-rect-length-percentage-auto": "LengthPercentageAutoInput | PartialRectInput", "partial-size-dimension": "DimensionInput | PartialSizeInput", + "partial-size-length-percentage-auto": + "LengthPercentageAutoInput | PartialSizeInput", "partial-rect-length-percentage": "LengthPercentageInput | PartialRectInput", "partial-size-length-percentage": "LengthPercentageInput | PartialSizeInput", + "unsigned-16": "number", dimension: "DimensionInput", "grid-template-component-array": "readonly GridTemplateComponentInput[]", "track-sizing-array": "readonly TrackSizingFunctionInput[]", @@ -54,10 +57,14 @@ function encoderCall(field: StyleField): string { return `encoder.partialRectLengthPercentageAuto(value, ${name});`; case "partial-size-dimension": return `encoder.partialSizeDimension(value, ${name});`; + case "partial-size-length-percentage-auto": + return `encoder.partialSizeLengthPercentageAuto(value, ${name});`; case "partial-rect-length-percentage": return `encoder.partialRectLengthPercentage(value, ${name});`; case "partial-size-length-percentage": return `encoder.partialSizeLengthPercentage(value, ${name});`; + case "unsigned-16": + return `encoder.unsigned16(value, ${name});`; case "dimension": return `encoder.dimension(value, ${name});`; case "grid-template-component-array": diff --git a/tools/api-codegen/src/input/style-codec.ts b/tools/api-codegen/src/input/style-codec.ts index 73266fa..f92408a 100644 --- a/tools/api-codegen/src/input/style-codec.ts +++ b/tools/api-codegen/src/input/style-codec.ts @@ -9,8 +9,10 @@ export const styleEncodingCategories = [ "partial-point-enum", "partial-rect-length-percentage-auto", "partial-size-dimension", + "partial-size-length-percentage-auto", "partial-rect-length-percentage", "partial-size-length-percentage", + "unsigned-16", "dimension", "grid-template-component-array", "track-sizing-array",