flexily 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/package.json +24 -24
- package/src/CLAUDE.md +36 -21
- package/src/classic/layout.ts +107 -47
- package/src/classic/node.ts +60 -0
- package/src/constants.ts +2 -1
- package/src/index-classic.ts +1 -1
- package/src/index.ts +1 -1
- package/src/layout-flex-lines.ts +70 -3
- package/src/layout-helpers.ts +29 -9
- package/src/layout-stats.ts +0 -2
- package/src/layout-zero.ts +587 -160
- package/src/node-zero.ts +98 -2
- package/src/testing.ts +20 -14
- package/src/types.ts +22 -15
- package/src/utils.ts +47 -21
- package/dist/classic/layout.d.ts +0 -57
- package/dist/classic/layout.d.ts.map +0 -1
- package/dist/classic/layout.js +0 -1558
- package/dist/classic/layout.js.map +0 -1
- package/dist/classic/node.d.ts +0 -648
- package/dist/classic/node.d.ts.map +0 -1
- package/dist/classic/node.js +0 -1002
- package/dist/classic/node.js.map +0 -1
- package/dist/constants.d.ts +0 -58
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -70
- package/dist/constants.js.map +0 -1
- package/dist/index-classic.d.ts +0 -30
- package/dist/index-classic.d.ts.map +0 -1
- package/dist/index-classic.js +0 -57
- package/dist/index-classic.js.map +0 -1
- package/dist/index.d.ts +0 -30
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -57
- package/dist/index.js.map +0 -1
- package/dist/layout-flex-lines.d.ts +0 -77
- package/dist/layout-flex-lines.d.ts.map +0 -1
- package/dist/layout-flex-lines.js +0 -317
- package/dist/layout-flex-lines.js.map +0 -1
- package/dist/layout-helpers.d.ts +0 -48
- package/dist/layout-helpers.d.ts.map +0 -1
- package/dist/layout-helpers.js +0 -108
- package/dist/layout-helpers.js.map +0 -1
- package/dist/layout-measure.d.ts +0 -25
- package/dist/layout-measure.d.ts.map +0 -1
- package/dist/layout-measure.js +0 -231
- package/dist/layout-measure.js.map +0 -1
- package/dist/layout-stats.d.ts +0 -19
- package/dist/layout-stats.d.ts.map +0 -1
- package/dist/layout-stats.js +0 -37
- package/dist/layout-stats.js.map +0 -1
- package/dist/layout-traversal.d.ts +0 -28
- package/dist/layout-traversal.d.ts.map +0 -1
- package/dist/layout-traversal.js +0 -65
- package/dist/layout-traversal.js.map +0 -1
- package/dist/layout-zero.d.ts +0 -26
- package/dist/layout-zero.d.ts.map +0 -1
- package/dist/layout-zero.js +0 -1601
- package/dist/layout-zero.js.map +0 -1
- package/dist/logger.d.ts +0 -14
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -61
- package/dist/logger.js.map +0 -1
- package/dist/node-zero.d.ts +0 -702
- package/dist/node-zero.d.ts.map +0 -1
- package/dist/node-zero.js +0 -1268
- package/dist/node-zero.js.map +0 -1
- package/dist/testing.d.ts +0 -69
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -179
- package/dist/testing.js.map +0 -1
- package/dist/trace.d.ts +0 -74
- package/dist/trace.d.ts.map +0 -1
- package/dist/trace.js +0 -191
- package/dist/trace.js.map +0 -1
- package/dist/types.d.ts +0 -170
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -43
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts +0 -41
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -197
- package/dist/utils.js.map +0 -1
- package/src/beorn-logger.d.ts +0 -10
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
|
-
import { Node, FLEX_DIRECTION_ROW, DIRECTION_LTR } from "
|
|
9
|
+
import { Node, FLEX_DIRECTION_ROW, DIRECTION_LTR } from "flexily"
|
|
10
10
|
|
|
11
11
|
const root = Node.create()
|
|
12
12
|
root.setWidth(100)
|
|
@@ -34,7 +34,7 @@ console.log(child.getComputedWidth()) // 100
|
|
|
34
34
|
|
|
35
35
|
**No tree-shaking.** The WASM binary is monolithic. You get the entire engine even if you use a fraction of the features.
|
|
36
36
|
|
|
37
|
-
Facebook's original pure-JS flexbox engine (`css-layout`) was abandoned when they moved to C++. [flexbox.js](https://github.com/Planning-nl/flexbox.js) exists but is unmaintained and missing features. Flexily fills the gap:
|
|
37
|
+
Facebook's original pure-JS flexbox engine (`css-layout`) was abandoned when they moved to C++. [flexbox.js](https://github.com/Planning-nl/flexbox.js) exists but is unmaintained and missing features. Flexily fills the gap: comprehensive CSS flexbox support, Yoga-compatible API, pure JS, zero WASM.
|
|
38
38
|
|
|
39
39
|
## Who Should Use Flexily
|
|
40
40
|
|
|
@@ -45,11 +45,11 @@ Most developers should use a framework built on Flexily, not Flexily directly. F
|
|
|
45
45
|
- **Specialized tools** where you need direct control over layout computation
|
|
46
46
|
- **Anyone replacing Yoga** who wants a drop-in pure-JS alternative
|
|
47
47
|
|
|
48
|
-
> **Building a terminal UI?** Use [silvery](https://
|
|
48
|
+
> **Building a terminal UI?** Use [silvery](https://silvery.dev), which uses Flexily by default. You get React components, hooks, and layout feedback without touching the low-level API.
|
|
49
49
|
|
|
50
50
|
## Status
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
1495 tests, including 44 Yoga compatibility tests and 1200+ incremental re-layout fuzz tests. Used by [silvery](https://silvery.dev) as its default layout engine.
|
|
53
53
|
|
|
54
54
|
| Feature | Status |
|
|
55
55
|
| --------------------------------------------- | -------- |
|
|
@@ -68,11 +68,11 @@ Most developers should use a framework built on Flexily, not Flexily directly. F
|
|
|
68
68
|
## Installation
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
|
|
72
|
-
# or
|
|
73
|
-
npm install @beorn/flexily
|
|
71
|
+
npm install flexily
|
|
74
72
|
```
|
|
75
73
|
|
|
74
|
+
**Runtimes:** Bun >= 1.0, Node.js >= 18. Pure JavaScript — no native or WASM dependencies.
|
|
75
|
+
|
|
76
76
|
## Performance
|
|
77
77
|
|
|
78
78
|
Flexily and Yoga each win in different scenarios:
|
|
@@ -107,13 +107,13 @@ See [docs/performance.md](docs/performance.md) for detailed benchmarks including
|
|
|
107
107
|
|
|
108
108
|
Flexily provides two layout implementations that produce identical output and pass identical tests:
|
|
109
109
|
|
|
110
|
-
**Zero-allocation** (default,
|
|
110
|
+
**Zero-allocation** (default, `flexily`): Mutates `FlexInfo` structs on nodes instead of allocating temporary objects. Faster for flat/wide trees typical of TUI layouts. Re-entrant via save/restore of scratch arrays (supports nested `calculateLayout()` calls from measure/baseline functions).
|
|
111
111
|
|
|
112
|
-
**Classic** (
|
|
112
|
+
**Classic** (`flexily/classic`): Allocates temporary objects during layout. Easier to read and debug. Use this when stepping through the algorithm or comparing behavior.
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
|
-
import { Node } from "
|
|
116
|
-
import { Node } from "
|
|
115
|
+
import { Node } from "flexily" // zero-allocation (default)
|
|
116
|
+
import { Node } from "flexily/classic" // allocating (debugging)
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
Both implement CSS Flexbox spec Section 9.7 with iterative freeze for min/max constraints, Yoga-compatible edge-based rounding, weighted flex-shrink, auto margin absorption, and full RTL support.
|
|
@@ -124,7 +124,7 @@ Incremental re-layout (caching unchanged subtrees) is essential for performance
|
|
|
124
124
|
|
|
125
125
|
| Layer | Tests | What it verifies |
|
|
126
126
|
| ------------------ | --------- | -------------------------------------------------------------- |
|
|
127
|
-
| Yoga compatibility |
|
|
127
|
+
| Yoga compatibility | 44 | Identical output to Yoga for every feature |
|
|
128
128
|
| Feature tests | ~110 | Each flexbox feature in isolation |
|
|
129
129
|
| **Re-layout fuzz** | **1200+** | Incremental re-layout matches fresh layout across random trees |
|
|
130
130
|
|
|
@@ -143,7 +143,7 @@ See [docs/testing.md](docs/testing.md) for methodology and [docs/incremental-lay
|
|
|
143
143
|
|
|
144
144
|
## API Compatibility
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Yoga-compatible API (44 comparison tests passing). Near drop-in replacement for common use cases:
|
|
147
147
|
|
|
148
148
|
```typescript
|
|
149
149
|
// Yoga
|
|
@@ -152,7 +152,7 @@ const yoga = await Yoga.init() // Async!
|
|
|
152
152
|
const root = yoga.Node.create()
|
|
153
153
|
|
|
154
154
|
// Flexily
|
|
155
|
-
import { Node } from "
|
|
155
|
+
import { Node } from "flexily"
|
|
156
156
|
const root = Node.create() // Sync!
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -178,7 +178,7 @@ Same constants, same method names, same behavior.
|
|
|
178
178
|
| [Taffy](https://github.com/DioxusLabs/taffy) | Rust | High-performance layout library supporting Flexbox and CSS Grid. Used by Dioxus and Bevy. |
|
|
179
179
|
| [flexbox.js](https://github.com/Planning-nl/flexbox.js) | JavaScript | Pure JS flexbox engine by Planning-nl. Reference implementation that inspired Flexily's algorithm. |
|
|
180
180
|
| [css-layout](https://www.npmjs.com/package/css-layout) | JavaScript | Facebook's original pure-JS flexbox, predecessor to Yoga. Deprecated. |
|
|
181
|
-
| [silvery](https://
|
|
181
|
+
| [silvery](https://silvery.dev) | TypeScript | React for CLIs with layout feedback. Uses Flexily by default. |
|
|
182
182
|
|
|
183
183
|
## Code Structure
|
|
184
184
|
|
|
@@ -186,7 +186,7 @@ Same constants, same method names, same behavior.
|
|
|
186
186
|
src/
|
|
187
187
|
├── index.ts # Main export
|
|
188
188
|
├── node-zero.ts # Node class with FlexInfo
|
|
189
|
-
├── layout-zero.ts # Layout algorithm (~
|
|
189
|
+
├── layout-zero.ts # Layout algorithm (~2000 lines)
|
|
190
190
|
├── constants.ts # Flexbox constants (Yoga-compatible)
|
|
191
191
|
├── types.ts # TypeScript interfaces
|
|
192
192
|
├── utils.ts # Shared utilities
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flexily",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pure JavaScript flexbox layout engine
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Pure JavaScript flexbox layout engine \u2014 Yoga-compatible API, faster initial layout, smaller bundle, no WASM",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"canvas-ui",
|
|
6
7
|
"css",
|
|
7
8
|
"flexbox",
|
|
8
9
|
"layout",
|
|
10
|
+
"measure",
|
|
9
11
|
"pure-javascript",
|
|
10
12
|
"terminal",
|
|
11
13
|
"tui",
|
|
14
|
+
"ui-layout",
|
|
12
15
|
"yoga",
|
|
16
|
+
"yoga-alternative",
|
|
17
|
+
"yoga-wasm",
|
|
13
18
|
"zero-dependency"
|
|
14
19
|
],
|
|
15
20
|
"homepage": "https://beorn.github.io/flexily/",
|
|
@@ -23,46 +28,41 @@
|
|
|
23
28
|
"url": "https://github.com/beorn/flexily.git"
|
|
24
29
|
},
|
|
25
30
|
"files": [
|
|
26
|
-
"dist",
|
|
27
31
|
"src"
|
|
28
32
|
],
|
|
29
33
|
"type": "module",
|
|
30
|
-
"main": "./
|
|
31
|
-
"types": "./
|
|
34
|
+
"main": "./src/index.ts",
|
|
35
|
+
"types": "./src/index.ts",
|
|
32
36
|
"exports": {
|
|
33
37
|
".": {
|
|
34
|
-
"types": "./
|
|
35
|
-
"import": "./
|
|
38
|
+
"types": "./src/index.ts",
|
|
39
|
+
"import": "./src/index.ts"
|
|
36
40
|
},
|
|
37
41
|
"./classic": {
|
|
38
|
-
"types": "./
|
|
39
|
-
"import": "./
|
|
42
|
+
"types": "./src/classic/index.ts",
|
|
43
|
+
"import": "./src/classic/index.ts"
|
|
40
44
|
}
|
|
41
45
|
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
42
49
|
"scripts": {
|
|
43
50
|
"build": "tsc",
|
|
44
51
|
"test": "bun test",
|
|
45
52
|
"test:watch": "bun test --watch",
|
|
46
53
|
"bench": "bunx --bun vitest bench",
|
|
47
|
-
"bench:standalone": "bun bench/run.ts",
|
|
48
|
-
"bench:compare": "bun bench/compare.ts",
|
|
49
54
|
"typecheck": "tsc --noEmit",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"debug": "^4.4.3"
|
|
55
|
+
"docs:dev": "vitepress dev docs",
|
|
56
|
+
"docs:build": "vitepress build docs",
|
|
57
|
+
"docs:preview": "vitepress preview docs"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"typescript": "^5.7.3",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"vitepress": "^1.6.3",
|
|
62
|
+
"vitest": "^3.1.0",
|
|
62
63
|
"yoga-wasm-web": "^0.3.3"
|
|
63
64
|
},
|
|
64
65
|
"engines": {
|
|
65
|
-
"
|
|
66
|
-
"node": ">=18"
|
|
66
|
+
"node": ">=23.6.0"
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|
package/src/CLAUDE.md
CHANGED
|
@@ -16,16 +16,16 @@ Flexily is a pure-JavaScript flexbox layout engine with a Yoga-compatible API. T
|
|
|
16
16
|
│ │
|
|
17
17
|
┌──────┴──────┐ ┌──────┴───────┐
|
|
18
18
|
│ node-zero.ts│ │layout-zero.ts │
|
|
19
|
-
│ (1412 LOC) │ │ (
|
|
19
|
+
│ (1412 LOC) │ │ (2029 LOC) │
|
|
20
20
|
│ Node class │ │ layoutNode │
|
|
21
21
|
└──────┬──────┘ └──────┬────────┘
|
|
22
22
|
│ │
|
|
23
23
|
┌──────┴──────┐ ┌──────────────────┼──────────────────┐
|
|
24
24
|
│ types.ts │ │ │ │
|
|
25
25
|
│ Interfaces │ layout-helpers.ts layout-flex-lines.ts │
|
|
26
|
-
└─────────────┘ (
|
|
26
|
+
└─────────────┘ (160 LOC) (349 LOC) │
|
|
27
27
|
Edge resolution Pre-alloc arrays layout-measure.ts
|
|
28
|
-
Line breaking (
|
|
28
|
+
Line breaking (259 LOC)
|
|
29
29
|
Flex distribution measureNode
|
|
30
30
|
|
|
31
31
|
layout-traversal.ts (70 LOC) - Tree traversal (markSubtreeLayoutSeen, countNodes)
|
|
@@ -42,22 +42,22 @@ Flexily is a pure-JavaScript flexbox layout engine with a Yoga-compatible API. T
|
|
|
42
42
|
- Factory function API: `Node.create()` (no `new` in user code, though `Node` is a class internally)
|
|
43
43
|
- Yoga-compatible API surface: same method names, same constants, drop-in replacement
|
|
44
44
|
- Pure JavaScript: no WASM, no native dependencies, synchronous initialization
|
|
45
|
-
-
|
|
45
|
+
- Module-level pre-allocated arrays with save/restore for re-entrancy (measureFunc/baselineFunc may call calculateLayout on other trees)
|
|
46
46
|
|
|
47
47
|
## Source Files
|
|
48
48
|
|
|
49
49
|
| File | LOC | Role | Hot path? |
|
|
50
50
|
| ---------------------- | ----- | -------------------------------------------------------------------- | ------------------------------ |
|
|
51
|
-
| `layout-zero.ts` |
|
|
52
|
-
| `layout-helpers.ts` |
|
|
53
|
-
| `layout-flex-lines.ts` |
|
|
54
|
-
| `layout-measure.ts` |
|
|
51
|
+
| `layout-zero.ts` | 2029 | Core layout: `computeLayout()`, `layoutNode()` (11 phases) | **Yes** - most critical |
|
|
52
|
+
| `layout-helpers.ts` | 160 | Edge resolution: margins, padding, borders | **Yes** - called per edge |
|
|
53
|
+
| `layout-flex-lines.ts` | 349 | Pre-alloc arrays, `breakIntoLines()`, `distributeFlexSpaceForLine()` | **Yes** - flex distribution |
|
|
54
|
+
| `layout-measure.ts` | 259 | `measureNode()` — intrinsic sizing | **Yes** - sizing pass |
|
|
55
55
|
| `layout-traversal.ts` | 70 | Tree traversal: `markSubtreeLayoutSeen()`, `countNodes()` | Moderate |
|
|
56
|
-
| `layout-stats.ts` |
|
|
56
|
+
| `layout-stats.ts` | 41 | Debug/benchmark counters | No (counters only) |
|
|
57
57
|
| `node-zero.ts` | 1412 | Node class, tree ops, caching | **Yes** - second most critical |
|
|
58
|
-
| `types.ts` |
|
|
59
|
-
| `utils.ts` |
|
|
60
|
-
| `constants.ts` |
|
|
58
|
+
| `types.ts` | 232 | `FlexInfo`, `Style`, `Layout`, `Value` interfaces | No (types only) |
|
|
59
|
+
| `utils.ts` | 240 | `resolveValue`, `applyMinMax`, edge helpers, shared traversal stack | Yes (called frequently) |
|
|
60
|
+
| `constants.ts` | 82 | Yoga-compatible numeric constants | No |
|
|
61
61
|
| `logger.ts` | 67 | Conditional debug logger (`log.debug?.()`) | No (conditional) |
|
|
62
62
|
| `testing.ts` | 209 | `getLayout`, `diffLayouts`, `expectRelayoutMatchesFresh` | No (test only) |
|
|
63
63
|
| `classic/` | ~2900 | Allocating reference algorithm | No (debugging only) |
|
|
@@ -125,6 +125,7 @@ Single pass over children:
|
|
|
125
125
|
### Phase 7a: Estimate Line Cross Sizes
|
|
126
126
|
|
|
127
127
|
- Tentative cross-axis sizes from definite child dimensions
|
|
128
|
+
- For measureFunc children: uses `child.flex.mainSize` (from flex distribution), not parent `mainAxisSize`. This ensures text wrapping is measured at the child's actual constrained width.
|
|
128
129
|
- Auto-sized children use 0 (actual size computed in Phase 8)
|
|
129
130
|
|
|
130
131
|
### Phase 7b: Apply alignContent
|
|
@@ -146,6 +147,14 @@ The most complex phase. For each relative child:
|
|
|
146
147
|
8. Apply cross-axis alignment offset (flex-end, center, baseline)
|
|
147
148
|
9. Advance `mainPos` for next child
|
|
148
149
|
|
|
150
|
+
### Phase 9b: Re-stretch Auto-Sized Cross Axis
|
|
151
|
+
|
|
152
|
+
- When cross axis was auto (NaN) during Phase 8, `ALIGN_STRETCH` children need re-stretching to the now-known cross size
|
|
153
|
+
|
|
154
|
+
### Phase 9c: Re-align Auto-Sized Cross Axis
|
|
155
|
+
|
|
156
|
+
- When `crossAxisSize` was NaN during Phase 8, alignment offsets for `ALIGN_CENTER`, `ALIGN_FLEX_END`, and cross-axis auto margins computed NaN. Phase 9c recomputes these using the final shrink-wrapped cross size.
|
|
157
|
+
|
|
149
158
|
### Phase 9: Shrink-Wrap Auto-Sized Containers
|
|
150
159
|
|
|
151
160
|
- For containers without explicit size, compute actual used space from children
|
|
@@ -184,7 +193,7 @@ let _lineItemSpacings = new Float64Array(32) // Per-line item spacing
|
|
|
184
193
|
|
|
185
194
|
These grow dynamically if >32 lines (rare). Total memory: ~1,344 bytes (4 Float64Arrays × 256 bytes + 1 Uint16Array × 64 bytes + Array overhead).
|
|
186
195
|
|
|
187
|
-
**
|
|
196
|
+
**Re-entrancy**: A `measureFunc` or `baselineFunc` may synchronously call `calculateLayout()` on a separate tree. `enterLayout()`/`exitLayout()` in `layout-flex-lines.ts` bracket nested calls to save and restore the module-level scratch arrays, preventing corruption of the outer pass's state. Only allocates on re-entrant calls (depth > 0).
|
|
188
197
|
|
|
189
198
|
### Per-Node FlexInfo (`node.flex`)
|
|
190
199
|
|
|
@@ -218,6 +227,8 @@ interface FlexInfo {
|
|
|
218
227
|
lastAvailH
|
|
219
228
|
lastOffsetX
|
|
220
229
|
lastOffsetY
|
|
230
|
+
lastAbsX
|
|
231
|
+
lastAbsY
|
|
221
232
|
lastDir
|
|
222
233
|
layoutValid
|
|
223
234
|
}
|
|
@@ -275,12 +286,16 @@ flex.lastAvailW = availableWidth
|
|
|
275
286
|
flex.lastAvailH = availableHeight
|
|
276
287
|
flex.lastOffsetX = offsetX
|
|
277
288
|
flex.lastOffsetY = offsetY
|
|
289
|
+
flex.lastAbsX = absX
|
|
290
|
+
flex.lastAbsY = absY
|
|
278
291
|
flex.lastDir = direction
|
|
279
292
|
flex.layoutValid = true
|
|
280
293
|
```
|
|
281
294
|
|
|
282
295
|
On next call, if `layoutValid && !isDirty && same constraints`, the entire subtree is skipped. Only position delta is propagated (if offset changed).
|
|
283
296
|
|
|
297
|
+
**`absX`/`absY` must be fingerprinted** because edge-based rounding depends on absolute position: `width = round(absX + nodeWidth) - round(absX)`. A fractional shift in absX (e.g., from a sibling's width change) changes the rounded result even when availW/availH/direction are unchanged.
|
|
298
|
+
|
|
284
299
|
**`Object.is()` is required** for NaN-safe comparison. `NaN === NaN` is `false`; `Object.is(NaN, NaN)` is `true`. NaN represents "unconstrained" -- a legitimate and common constraint value.
|
|
285
300
|
|
|
286
301
|
### Invalidation Triggers
|
|
@@ -316,11 +331,11 @@ This is Yoga's algorithm. Layout positions stored in `layout.left`/`layout.top`
|
|
|
316
331
|
|
|
317
332
|
## measureNode vs layoutNode
|
|
318
333
|
|
|
319
|
-
`measureNode()` (~
|
|
334
|
+
`measureNode()` (~260 lines) is a lightweight alternative to `layoutNode()` (~1900 lines). It computes `width` and `height` but NOT `left`/`top`. Used during Phase 5 for intrinsic sizing of auto-sized container children. Save/restore of `layout.width`/`layout.height` is required around `measureNode` calls because it overwrites those fields.
|
|
320
335
|
|
|
321
|
-
## Integration: How
|
|
336
|
+
## Integration: How Silvery Uses Flexily
|
|
322
337
|
|
|
323
|
-
|
|
338
|
+
Silvery uses Flexily through an adapter layer:
|
|
324
339
|
|
|
325
340
|
1. `silvery/src/layout-engine.ts` defines the `LayoutEngine` / `LayoutNode` interfaces
|
|
326
341
|
2. `silvery/src/adapters/flexily-zero-adapter.ts` wraps `Node` in `FlexilyZeroNodeAdapter`
|
|
@@ -335,7 +350,7 @@ silvery calls `calculateLayout()` on every render. The no-change case (cursor mo
|
|
|
335
350
|
| ----------------------------------------- | ---------------------------------------- | ------------------------------------- | ---------------------------------------------- |
|
|
336
351
|
| `overflow:hidden/scroll` + `flexShrink:0` | Item expands to content (ignores parent) | Item shrinks to fit parent | 4.5: auto min-size = 0 for overflow containers |
|
|
337
352
|
| Default `flexShrink` | 0 (Yoga native default) | 0 (matches Yoga) | CSS default is 1 |
|
|
338
|
-
| Default `flexDirection` | Column |
|
|
353
|
+
| Default `flexDirection` | Column | Row (CSS default) | Row |
|
|
339
354
|
| Baseline alignment | Full spec (recursive first-child) | Simplified (no recursive propagation) | Recursive first-child |
|
|
340
355
|
|
|
341
356
|
The `flexShrink` override for overflow containers (line ~1244 in layout-zero.ts) is the most significant divergence. Without it, `overflow:hidden` children inside constrained parents balloon to content size, defeating the purpose of clipping.
|
|
@@ -362,10 +377,10 @@ Edge-based properties use 6-element arrays: `[left, top, right, bottom, start, e
|
|
|
362
377
|
|
|
363
378
|
Border widths are plain numbers (always points). Logical border slots use `NaN` as "not set" sentinel.
|
|
364
379
|
|
|
365
|
-
### Default Style
|
|
380
|
+
### Default Style
|
|
366
381
|
|
|
367
382
|
```typescript
|
|
368
|
-
flexDirection:
|
|
383
|
+
flexDirection: ROW // CSS default (diverges from Yoga's COLUMN)
|
|
369
384
|
flexShrink: 0 // CSS default is 1
|
|
370
385
|
alignItems: STRETCH // Same as CSS
|
|
371
386
|
flexBasis: AUTO // Same as CSS
|
|
@@ -451,11 +466,11 @@ cd vendor/flexily && bun run build
|
|
|
451
466
|
|
|
452
467
|
| Layer | Tests | Command | What it verifies |
|
|
453
468
|
| ------------------ | --------- | ---------------------------------------------------------------------------- | --------------------------------- |
|
|
454
|
-
| Yoga compat |
|
|
469
|
+
| Yoga compat | 44 | `bun test tests/yoga-comparison.test.ts tests/yoga-overflow-compare.test.ts` | Identical output to Yoga |
|
|
455
470
|
| Feature tests | ~110 | `bun test tests/layout.test.ts` | Each flexbox feature in isolation |
|
|
456
471
|
| **Re-layout fuzz** | **1200+** | `bun test tests/relayout-consistency.test.ts` | Incremental matches fresh |
|
|
457
472
|
| Mutation testing | 4+ | `bun scripts/mutation-test.ts` | Fuzz catches cache mutations |
|
|
458
|
-
| All tests |
|
|
473
|
+
| All tests | 1495 | `bun test` | Everything |
|
|
459
474
|
|
|
460
475
|
The fuzz tests are the most important layer. They've caught 3 distinct caching bugs that all single-pass tests missed.
|
|
461
476
|
|
package/src/classic/layout.ts
CHANGED
|
@@ -123,9 +123,9 @@ export function resolveEdgeBorderValue(
|
|
|
123
123
|
|
|
124
124
|
// Logical takes precedence if set (NaN = not set)
|
|
125
125
|
if (logicalSlot !== undefined && !Number.isNaN(arr[logicalSlot])) {
|
|
126
|
-
return arr[logicalSlot]
|
|
126
|
+
return arr[logicalSlot]!
|
|
127
127
|
}
|
|
128
|
-
return arr[physicalIndex]
|
|
128
|
+
return arr[physicalIndex]!
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export function markSubtreeLayoutSeen(node: Node): void {
|
|
@@ -205,7 +205,10 @@ function breakIntoLines(children: ChildLayout[], mainAxisSize: number, mainGap:
|
|
|
205
205
|
let lineMainSize = 0
|
|
206
206
|
|
|
207
207
|
for (const child of children) {
|
|
208
|
-
|
|
208
|
+
// CSS spec 9.3.4: line breaking uses the "hypothetical main size" which is
|
|
209
|
+
// the flex base size clamped to min/max, not the unclamped base size.
|
|
210
|
+
const hypotheticalMainSize = Math.max(child.minMain, Math.min(child.maxMain, child.baseSize))
|
|
211
|
+
const childMainSize = hypotheticalMainSize + child.mainMargin
|
|
209
212
|
const gapIfNotFirst = currentLine.length > 0 ? mainGap : 0
|
|
210
213
|
|
|
211
214
|
// Check if child fits on current line
|
|
@@ -528,7 +531,9 @@ function layoutNode(
|
|
|
528
531
|
// This ensures children's absolute positions include parent's position offset
|
|
529
532
|
let parentPosOffsetX = 0
|
|
530
533
|
let parentPosOffsetY = 0
|
|
531
|
-
|
|
534
|
+
// CSS spec: position:static ignores insets (top/left/right/bottom).
|
|
535
|
+
// Only position:relative applies insets as offsets from normal flow position.
|
|
536
|
+
if (style.positionType === C.POSITION_TYPE_RELATIVE) {
|
|
532
537
|
const leftPos = style.position[0]
|
|
533
538
|
const topPos = style.position[1]
|
|
534
539
|
const rightPos = style.position[2]
|
|
@@ -935,6 +940,11 @@ function layoutNode(
|
|
|
935
940
|
childCross = crossDim.value
|
|
936
941
|
} else if (crossDim.unit === C.UNIT_PERCENT && !Number.isNaN(crossAxisSize)) {
|
|
937
942
|
childCross = crossAxisSize * (crossDim.value / 100)
|
|
943
|
+
} else if (childLayout.node.children.length > 0) {
|
|
944
|
+
// Auto-sized container children: Phase 5 already laid them out with
|
|
945
|
+
// layoutNode to compute baseSize. The cross-axis size is available
|
|
946
|
+
// on the child's layout (height for row, width for column).
|
|
947
|
+
childCross = isRow ? childLayout.node.layout.height : childLayout.node.layout.width
|
|
938
948
|
} else {
|
|
939
949
|
// Auto - use a default or measure. For now, use 0 and let stretch handle it.
|
|
940
950
|
childCross = 0
|
|
@@ -990,6 +1000,16 @@ function layoutNode(
|
|
|
990
1000
|
}
|
|
991
1001
|
break
|
|
992
1002
|
|
|
1003
|
+
case C.ALIGN_SPACE_EVENLY:
|
|
1004
|
+
// Equal spacing between lines and at edges
|
|
1005
|
+
if (numLines > 0) {
|
|
1006
|
+
const spaceEvenlyGap = freeSpace / (numLines + 1)
|
|
1007
|
+
for (let i = 0; i < numLines; i++) {
|
|
1008
|
+
lineCrossOffsets[i]! += spaceEvenlyGap * (i + 1)
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
break
|
|
1012
|
+
|
|
993
1013
|
case C.ALIGN_STRETCH:
|
|
994
1014
|
// Distribute extra space evenly among lines
|
|
995
1015
|
if (freeSpace > 0 && numLines > 0) {
|
|
@@ -1251,11 +1271,12 @@ function layoutNode(
|
|
|
1251
1271
|
const fractionalLeft = innerLeft + childX
|
|
1252
1272
|
const fractionalTop = innerTop + childY
|
|
1253
1273
|
|
|
1254
|
-
// Compute position offsets for RELATIVE
|
|
1274
|
+
// Compute position offsets for RELATIVE positioned children
|
|
1275
|
+
// CSS spec: position:static ignores insets; only position:relative applies them.
|
|
1255
1276
|
// These must be included in the absolute position BEFORE rounding (Yoga-compatible)
|
|
1256
1277
|
let posOffsetX = 0
|
|
1257
1278
|
let posOffsetY = 0
|
|
1258
|
-
if (childStyle.positionType === C.POSITION_TYPE_RELATIVE
|
|
1279
|
+
if (childStyle.positionType === C.POSITION_TYPE_RELATIVE) {
|
|
1259
1280
|
const relLeftPos = childStyle.position[0]
|
|
1260
1281
|
const relTopPos = childStyle.position[1]
|
|
1261
1282
|
const relRightPos = childStyle.position[2]
|
|
@@ -1469,7 +1490,7 @@ function layoutNode(
|
|
|
1469
1490
|
}
|
|
1470
1491
|
}
|
|
1471
1492
|
|
|
1472
|
-
if (crossOffset
|
|
1493
|
+
if (crossOffset !== 0) {
|
|
1473
1494
|
if (isRow) {
|
|
1474
1495
|
child.layout.top += Math.round(crossOffset)
|
|
1475
1496
|
} else {
|
|
@@ -1605,6 +1626,7 @@ function layoutNode(
|
|
|
1605
1626
|
// Content box dimensions for percentage resolution of absolute children
|
|
1606
1627
|
const absContentBoxW = absPaddingBoxW - paddingLeft - paddingRight
|
|
1607
1628
|
const absContentBoxH = absPaddingBoxH - paddingTop - paddingBottom
|
|
1629
|
+
const isRow = isRowDirection(style.flexDirection)
|
|
1608
1630
|
|
|
1609
1631
|
for (const child of absoluteChildren) {
|
|
1610
1632
|
const childStyle = child.style
|
|
@@ -1626,10 +1648,11 @@ function layoutNode(
|
|
|
1626
1648
|
const hasTop = topPos.unit !== C.UNIT_UNDEFINED
|
|
1627
1649
|
const hasBottom = bottomPos.unit !== C.UNIT_UNDEFINED
|
|
1628
1650
|
|
|
1629
|
-
|
|
1630
|
-
const
|
|
1631
|
-
const
|
|
1632
|
-
const
|
|
1651
|
+
// Yoga resolves percentage position offsets against the content box dimensions
|
|
1652
|
+
const leftOffset = resolveValue(leftPos, absContentBoxW)
|
|
1653
|
+
const topOffset = resolveValue(topPos, absContentBoxH)
|
|
1654
|
+
const rightOffset = resolveValue(rightPos, absContentBoxW)
|
|
1655
|
+
const bottomOffset = resolveValue(bottomPos, absContentBoxH)
|
|
1633
1656
|
|
|
1634
1657
|
// Calculate available size for absolute child using padding box
|
|
1635
1658
|
const contentW = absPaddingBoxW
|
|
@@ -1702,28 +1725,45 @@ function layoutNode(
|
|
|
1702
1725
|
const childHeight = child.layout.height
|
|
1703
1726
|
|
|
1704
1727
|
// Apply alignment when no explicit position set
|
|
1705
|
-
// For absolute children, align-items
|
|
1728
|
+
// For absolute children, align-items applies on cross axis, justify-content on main axis
|
|
1729
|
+
// Row: X = main axis (justifyContent), Y = cross axis (alignItems)
|
|
1730
|
+
// Column: X = cross axis (alignItems), Y = main axis (justifyContent)
|
|
1706
1731
|
if (!hasLeft && !hasRight) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1732
|
+
if (isRow) {
|
|
1733
|
+
// Row: X is main axis, use justifyContent
|
|
1734
|
+
const freeSpaceX = contentW - childWidth - childMarginLeft - childMarginRight
|
|
1735
|
+
switch (style.justifyContent) {
|
|
1736
|
+
case C.JUSTIFY_CENTER:
|
|
1737
|
+
childX = childMarginLeft + freeSpaceX / 2
|
|
1738
|
+
break
|
|
1739
|
+
case C.JUSTIFY_FLEX_END:
|
|
1740
|
+
childX = childMarginLeft + freeSpaceX
|
|
1741
|
+
break
|
|
1742
|
+
default: // FLEX_START
|
|
1743
|
+
childX = childMarginLeft
|
|
1744
|
+
break
|
|
1745
|
+
}
|
|
1746
|
+
} else {
|
|
1747
|
+
// Column: X is cross axis, use alignItems/alignSelf
|
|
1748
|
+
let alignment = style.alignItems
|
|
1749
|
+
if (childStyle.alignSelf !== C.ALIGN_AUTO) {
|
|
1750
|
+
alignment = childStyle.alignSelf
|
|
1751
|
+
}
|
|
1752
|
+
const freeSpaceX = contentW - childWidth - childMarginLeft - childMarginRight
|
|
1753
|
+
switch (alignment) {
|
|
1754
|
+
case C.ALIGN_CENTER:
|
|
1755
|
+
childX = childMarginLeft + freeSpaceX / 2
|
|
1756
|
+
break
|
|
1757
|
+
case C.ALIGN_FLEX_END:
|
|
1758
|
+
childX = childMarginLeft + freeSpaceX
|
|
1759
|
+
break
|
|
1760
|
+
case C.ALIGN_STRETCH:
|
|
1761
|
+
// Stretch: already handled by setting width to fill
|
|
1762
|
+
break
|
|
1763
|
+
default: // FLEX_START
|
|
1764
|
+
childX = childMarginLeft
|
|
1765
|
+
break
|
|
1766
|
+
}
|
|
1727
1767
|
}
|
|
1728
1768
|
} else if (!hasLeft && hasRight) {
|
|
1729
1769
|
// Position from right edge
|
|
@@ -1734,19 +1774,41 @@ function layoutNode(
|
|
|
1734
1774
|
}
|
|
1735
1775
|
|
|
1736
1776
|
if (!hasTop && !hasBottom) {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1777
|
+
if (isRow) {
|
|
1778
|
+
// Row: Y is cross axis, use alignItems/alignSelf
|
|
1779
|
+
let alignment = style.alignItems
|
|
1780
|
+
if (childStyle.alignSelf !== C.ALIGN_AUTO) {
|
|
1781
|
+
alignment = childStyle.alignSelf
|
|
1782
|
+
}
|
|
1783
|
+
const freeSpaceY = contentH - childHeight - childMarginTop - childMarginBottom
|
|
1784
|
+
switch (alignment) {
|
|
1785
|
+
case C.ALIGN_CENTER:
|
|
1786
|
+
childY = childMarginTop + freeSpaceY / 2
|
|
1787
|
+
break
|
|
1788
|
+
case C.ALIGN_FLEX_END:
|
|
1789
|
+
childY = childMarginTop + freeSpaceY
|
|
1790
|
+
break
|
|
1791
|
+
case C.ALIGN_STRETCH:
|
|
1792
|
+
// Stretch: already handled by setting height to fill
|
|
1793
|
+
break
|
|
1794
|
+
default: // FLEX_START
|
|
1795
|
+
childY = childMarginTop
|
|
1796
|
+
break
|
|
1797
|
+
}
|
|
1798
|
+
} else {
|
|
1799
|
+
// Column: Y is main axis, use justifyContent
|
|
1800
|
+
const freeSpaceY = contentH - childHeight - childMarginTop - childMarginBottom
|
|
1801
|
+
switch (style.justifyContent) {
|
|
1802
|
+
case C.JUSTIFY_CENTER:
|
|
1803
|
+
childY = childMarginTop + freeSpaceY / 2
|
|
1804
|
+
break
|
|
1805
|
+
case C.JUSTIFY_FLEX_END:
|
|
1806
|
+
childY = childMarginTop + freeSpaceY
|
|
1807
|
+
break
|
|
1808
|
+
default: // FLEX_START
|
|
1809
|
+
childY = childMarginTop
|
|
1810
|
+
break
|
|
1811
|
+
}
|
|
1750
1812
|
}
|
|
1751
1813
|
} else if (!hasTop && hasBottom) {
|
|
1752
1814
|
// Position from bottom edge
|
|
@@ -1769,14 +1831,12 @@ function layoutNode(
|
|
|
1769
1831
|
// so the public API remains consistent between versions.
|
|
1770
1832
|
|
|
1771
1833
|
export let layoutNodeCalls = 0
|
|
1772
|
-
export let resolveEdgeCalls = 0
|
|
1773
1834
|
export let layoutSizingCalls = 0
|
|
1774
1835
|
export let layoutPositioningCalls = 0
|
|
1775
1836
|
export let layoutCacheHits = 0
|
|
1776
1837
|
|
|
1777
1838
|
export function resetLayoutStats(): void {
|
|
1778
1839
|
layoutNodeCalls = 0
|
|
1779
|
-
resolveEdgeCalls = 0
|
|
1780
1840
|
layoutSizingCalls = 0
|
|
1781
1841
|
layoutPositioningCalls = 0
|
|
1782
1842
|
layoutCacheHits = 0
|