yzcode-cli 1.0.1 → 1.0.3

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.
Files changed (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,2578 @@
1
+ /**
2
+ * Pure-TypeScript port of yoga-layout (Meta's flexbox engine).
3
+ *
4
+ * This matches the `yoga-layout/load` API surface used by src/ink/layout/yoga.ts.
5
+ * The upstream C++ source is ~2500 lines in CalculateLayout.cpp alone; this port
6
+ * is a simplified single-pass flexbox implementation that covers the subset of
7
+ * features Ink actually uses:
8
+ * - flex-direction (row/column + reverse)
9
+ * - flex-grow / flex-shrink / flex-basis
10
+ * - align-items / align-self (stretch, flex-start, center, flex-end)
11
+ * - justify-content (all six values)
12
+ * - margin / padding / border / gap
13
+ * - width / height / min / max (point, percent, auto)
14
+ * - position: relative / absolute
15
+ * - display: flex / none
16
+ * - measure functions (for text nodes)
17
+ *
18
+ * Also implemented for spec parity (not used by Ink):
19
+ * - margin: auto (main + cross axis, overrides justify/align)
20
+ * - multi-pass flex clamping when children hit min/max constraints
21
+ * - flex-grow/shrink against container min/max when size is indefinite
22
+ *
23
+ * Also implemented for spec parity (not used by Ink):
24
+ * - flex-wrap: wrap / wrap-reverse (multi-line flex)
25
+ * - align-content (positions wrapped lines on cross axis)
26
+ *
27
+ * Also implemented for spec parity (not used by Ink):
28
+ * - display: contents (children lifted to grandparent, box removed)
29
+ *
30
+ * Also implemented for spec parity (not used by Ink):
31
+ * - baseline alignment (align-items/align-self: baseline)
32
+ *
33
+ * Not implemented (not used by Ink):
34
+ * - aspect-ratio
35
+ * - box-sizing: content-box
36
+ * - RTL direction (Ink always passes Direction.LTR)
37
+ *
38
+ * Upstream: https://github.com/facebook/yoga
39
+ */
40
+
41
+ import {
42
+ Align,
43
+ BoxSizing,
44
+ Dimension,
45
+ Direction,
46
+ Display,
47
+ Edge,
48
+ Errata,
49
+ ExperimentalFeature,
50
+ FlexDirection,
51
+ Gutter,
52
+ Justify,
53
+ MeasureMode,
54
+ Overflow,
55
+ PositionType,
56
+ Unit,
57
+ Wrap,
58
+ } from './enums.js'
59
+
60
+ export {
61
+ Align,
62
+ BoxSizing,
63
+ Dimension,
64
+ Direction,
65
+ Display,
66
+ Edge,
67
+ Errata,
68
+ ExperimentalFeature,
69
+ FlexDirection,
70
+ Gutter,
71
+ Justify,
72
+ MeasureMode,
73
+ Overflow,
74
+ PositionType,
75
+ Unit,
76
+ Wrap,
77
+ }
78
+
79
+ // --
80
+ // Value types
81
+
82
+ export type Value = {
83
+ unit: Unit
84
+ value: number
85
+ }
86
+
87
+ const UNDEFINED_VALUE: Value = { unit: Unit.Undefined, value: NaN }
88
+ const AUTO_VALUE: Value = { unit: Unit.Auto, value: NaN }
89
+
90
+ function pointValue(v: number): Value {
91
+ return { unit: Unit.Point, value: v }
92
+ }
93
+ function percentValue(v: number): Value {
94
+ return { unit: Unit.Percent, value: v }
95
+ }
96
+
97
+ function resolveValue(v: Value, ownerSize: number): number {
98
+ switch (v.unit) {
99
+ case Unit.Point:
100
+ return v.value
101
+ case Unit.Percent:
102
+ return isNaN(ownerSize) ? NaN : (v.value * ownerSize) / 100
103
+ default:
104
+ return NaN
105
+ }
106
+ }
107
+
108
+ function isDefined(n: number): boolean {
109
+ return !isNaN(n)
110
+ }
111
+
112
+ // NaN-safe equality for layout-cache input comparison
113
+ function sameFloat(a: number, b: number): boolean {
114
+ return a === b || (a !== a && b !== b)
115
+ }
116
+
117
+ // --
118
+ // Layout result (computed values)
119
+
120
+ type Layout = {
121
+ left: number
122
+ top: number
123
+ width: number
124
+ height: number
125
+ // Computed per-edge values (resolved to physical edges)
126
+ border: [number, number, number, number] // left, top, right, bottom
127
+ padding: [number, number, number, number]
128
+ margin: [number, number, number, number]
129
+ }
130
+
131
+ // --
132
+ // Style (input values)
133
+
134
+ type Style = {
135
+ direction: Direction
136
+ flexDirection: FlexDirection
137
+ justifyContent: Justify
138
+ alignItems: Align
139
+ alignSelf: Align
140
+ alignContent: Align
141
+ flexWrap: Wrap
142
+ overflow: Overflow
143
+ display: Display
144
+ positionType: PositionType
145
+
146
+ flexGrow: number
147
+ flexShrink: number
148
+ flexBasis: Value
149
+
150
+ // 9-edge arrays indexed by Edge enum
151
+ margin: Value[]
152
+ padding: Value[]
153
+ border: Value[]
154
+ position: Value[]
155
+
156
+ // 3-gutter array indexed by Gutter enum
157
+ gap: Value[]
158
+
159
+ width: Value
160
+ height: Value
161
+ minWidth: Value
162
+ minHeight: Value
163
+ maxWidth: Value
164
+ maxHeight: Value
165
+ }
166
+
167
+ function defaultStyle(): Style {
168
+ return {
169
+ direction: Direction.Inherit,
170
+ flexDirection: FlexDirection.Column,
171
+ justifyContent: Justify.FlexStart,
172
+ alignItems: Align.Stretch,
173
+ alignSelf: Align.Auto,
174
+ alignContent: Align.FlexStart,
175
+ flexWrap: Wrap.NoWrap,
176
+ overflow: Overflow.Visible,
177
+ display: Display.Flex,
178
+ positionType: PositionType.Relative,
179
+ flexGrow: 0,
180
+ flexShrink: 0,
181
+ flexBasis: AUTO_VALUE,
182
+ margin: new Array(9).fill(UNDEFINED_VALUE),
183
+ padding: new Array(9).fill(UNDEFINED_VALUE),
184
+ border: new Array(9).fill(UNDEFINED_VALUE),
185
+ position: new Array(9).fill(UNDEFINED_VALUE),
186
+ gap: new Array(3).fill(UNDEFINED_VALUE),
187
+ width: AUTO_VALUE,
188
+ height: AUTO_VALUE,
189
+ minWidth: UNDEFINED_VALUE,
190
+ minHeight: UNDEFINED_VALUE,
191
+ maxWidth: UNDEFINED_VALUE,
192
+ maxHeight: UNDEFINED_VALUE,
193
+ }
194
+ }
195
+
196
+ // --
197
+ // Edge resolution — yoga's 9-edge model collapsed to 4 physical edges
198
+
199
+ const EDGE_LEFT = 0
200
+ const EDGE_TOP = 1
201
+ const EDGE_RIGHT = 2
202
+ const EDGE_BOTTOM = 3
203
+
204
+ function resolveEdge(
205
+ edges: Value[],
206
+ physicalEdge: number,
207
+ ownerSize: number,
208
+ // For margin/position we allow auto; for padding/border auto resolves to 0
209
+ allowAuto = false,
210
+ ): number {
211
+ // Precedence: specific edge > horizontal/vertical > all
212
+ let v = edges[physicalEdge]!
213
+ if (v.unit === Unit.Undefined) {
214
+ if (physicalEdge === EDGE_LEFT || physicalEdge === EDGE_RIGHT) {
215
+ v = edges[Edge.Horizontal]!
216
+ } else {
217
+ v = edges[Edge.Vertical]!
218
+ }
219
+ }
220
+ if (v.unit === Unit.Undefined) {
221
+ v = edges[Edge.All]!
222
+ }
223
+ // Start/End map to Left/Right for LTR (Ink is always LTR)
224
+ if (v.unit === Unit.Undefined) {
225
+ if (physicalEdge === EDGE_LEFT) v = edges[Edge.Start]!
226
+ if (physicalEdge === EDGE_RIGHT) v = edges[Edge.End]!
227
+ }
228
+ if (v.unit === Unit.Undefined) return 0
229
+ if (v.unit === Unit.Auto) return allowAuto ? NaN : 0
230
+ return resolveValue(v, ownerSize)
231
+ }
232
+
233
+ function resolveEdgeRaw(edges: Value[], physicalEdge: number): Value {
234
+ let v = edges[physicalEdge]!
235
+ if (v.unit === Unit.Undefined) {
236
+ if (physicalEdge === EDGE_LEFT || physicalEdge === EDGE_RIGHT) {
237
+ v = edges[Edge.Horizontal]!
238
+ } else {
239
+ v = edges[Edge.Vertical]!
240
+ }
241
+ }
242
+ if (v.unit === Unit.Undefined) v = edges[Edge.All]!
243
+ if (v.unit === Unit.Undefined) {
244
+ if (physicalEdge === EDGE_LEFT) v = edges[Edge.Start]!
245
+ if (physicalEdge === EDGE_RIGHT) v = edges[Edge.End]!
246
+ }
247
+ return v
248
+ }
249
+
250
+ function isMarginAuto(edges: Value[], physicalEdge: number): boolean {
251
+ return resolveEdgeRaw(edges, physicalEdge).unit === Unit.Auto
252
+ }
253
+
254
+ // Setter helpers for the _hasAutoMargin / _hasPosition fast-path flags.
255
+ // Unit.Undefined = 0, Unit.Auto = 3.
256
+ function hasAnyAutoEdge(edges: Value[]): boolean {
257
+ for (let i = 0; i < 9; i++) if (edges[i]!.unit === 3) return true
258
+ return false
259
+ }
260
+ function hasAnyDefinedEdge(edges: Value[]): boolean {
261
+ for (let i = 0; i < 9; i++) if (edges[i]!.unit !== 0) return true
262
+ return false
263
+ }
264
+
265
+ // Hot path: resolve all 4 physical edges in one pass, writing into `out`.
266
+ // Equivalent to calling resolveEdge() 4× with allowAuto=false, but hoists the
267
+ // shared fallback lookups (Horizontal/Vertical/All/Start/End) and avoids
268
+ // allocating a fresh 4-array on every layoutNode() call.
269
+ function resolveEdges4Into(
270
+ edges: Value[],
271
+ ownerSize: number,
272
+ out: [number, number, number, number],
273
+ ): void {
274
+ // Hoist fallbacks once — the 4 per-edge chains share these reads.
275
+ const eH = edges[6]! // Edge.Horizontal
276
+ const eV = edges[7]! // Edge.Vertical
277
+ const eA = edges[8]! // Edge.All
278
+ const eS = edges[4]! // Edge.Start
279
+ const eE = edges[5]! // Edge.End
280
+ const pctDenom = isNaN(ownerSize) ? NaN : ownerSize / 100
281
+
282
+ // Left: edges[0] → Horizontal → All → Start
283
+ let v = edges[0]!
284
+ if (v.unit === 0) v = eH
285
+ if (v.unit === 0) v = eA
286
+ if (v.unit === 0) v = eS
287
+ out[0] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
288
+
289
+ // Top: edges[1] → Vertical → All
290
+ v = edges[1]!
291
+ if (v.unit === 0) v = eV
292
+ if (v.unit === 0) v = eA
293
+ out[1] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
294
+
295
+ // Right: edges[2] → Horizontal → All → End
296
+ v = edges[2]!
297
+ if (v.unit === 0) v = eH
298
+ if (v.unit === 0) v = eA
299
+ if (v.unit === 0) v = eE
300
+ out[2] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
301
+
302
+ // Bottom: edges[3] → Vertical → All
303
+ v = edges[3]!
304
+ if (v.unit === 0) v = eV
305
+ if (v.unit === 0) v = eA
306
+ out[3] = v.unit === 1 ? v.value : v.unit === 2 ? v.value * pctDenom : 0
307
+ }
308
+
309
+ // --
310
+ // Axis helpers
311
+
312
+ function isRow(dir: FlexDirection): boolean {
313
+ return dir === FlexDirection.Row || dir === FlexDirection.RowReverse
314
+ }
315
+ function isReverse(dir: FlexDirection): boolean {
316
+ return dir === FlexDirection.RowReverse || dir === FlexDirection.ColumnReverse
317
+ }
318
+ function crossAxis(dir: FlexDirection): FlexDirection {
319
+ return isRow(dir) ? FlexDirection.Column : FlexDirection.Row
320
+ }
321
+ function leadingEdge(dir: FlexDirection): number {
322
+ switch (dir) {
323
+ case FlexDirection.Row:
324
+ return EDGE_LEFT
325
+ case FlexDirection.RowReverse:
326
+ return EDGE_RIGHT
327
+ case FlexDirection.Column:
328
+ return EDGE_TOP
329
+ case FlexDirection.ColumnReverse:
330
+ return EDGE_BOTTOM
331
+ }
332
+ }
333
+ function trailingEdge(dir: FlexDirection): number {
334
+ switch (dir) {
335
+ case FlexDirection.Row:
336
+ return EDGE_RIGHT
337
+ case FlexDirection.RowReverse:
338
+ return EDGE_LEFT
339
+ case FlexDirection.Column:
340
+ return EDGE_BOTTOM
341
+ case FlexDirection.ColumnReverse:
342
+ return EDGE_TOP
343
+ }
344
+ }
345
+
346
+ // --
347
+ // Public types
348
+
349
+ export type MeasureFunction = (
350
+ width: number,
351
+ widthMode: MeasureMode,
352
+ height: number,
353
+ heightMode: MeasureMode,
354
+ ) => { width: number; height: number }
355
+
356
+ export type Size = { width: number; height: number }
357
+
358
+ // --
359
+ // Config
360
+
361
+ export type Config = {
362
+ pointScaleFactor: number
363
+ errata: Errata
364
+ useWebDefaults: boolean
365
+ free(): void
366
+ isExperimentalFeatureEnabled(_: ExperimentalFeature): boolean
367
+ setExperimentalFeatureEnabled(_: ExperimentalFeature, __: boolean): void
368
+ setPointScaleFactor(factor: number): void
369
+ getErrata(): Errata
370
+ setErrata(errata: Errata): void
371
+ setUseWebDefaults(v: boolean): void
372
+ }
373
+
374
+ function createConfig(): Config {
375
+ const config: Config = {
376
+ pointScaleFactor: 1,
377
+ errata: Errata.None,
378
+ useWebDefaults: false,
379
+ free() {},
380
+ isExperimentalFeatureEnabled() {
381
+ return false
382
+ },
383
+ setExperimentalFeatureEnabled() {},
384
+ setPointScaleFactor(f) {
385
+ config.pointScaleFactor = f
386
+ },
387
+ getErrata() {
388
+ return config.errata
389
+ },
390
+ setErrata(e) {
391
+ config.errata = e
392
+ },
393
+ setUseWebDefaults(v) {
394
+ config.useWebDefaults = v
395
+ },
396
+ }
397
+ return config
398
+ }
399
+
400
+ // --
401
+ // Node implementation
402
+
403
+ export class Node {
404
+ style: Style
405
+ layout: Layout
406
+ parent: Node | null
407
+ children: Node[]
408
+ measureFunc: MeasureFunction | null
409
+ config: Config
410
+ isDirty_: boolean
411
+ isReferenceBaseline_: boolean
412
+
413
+ // Per-layout scratch (not public API)
414
+ _flexBasis = 0
415
+ _mainSize = 0
416
+ _crossSize = 0
417
+ _lineIndex = 0
418
+ // Fast-path flags maintained by style setters. Per CPU profile, the
419
+ // positioning loop calls isMarginAuto 6× and resolveEdgeRaw(position) 4×
420
+ // per child per layout pass — ~11k calls for the 1000-node bench, nearly
421
+ // all of which return false/undefined since most nodes have no auto
422
+ // margins and no position insets. These flags let us skip straight to
423
+ // the common case with a single branch.
424
+ _hasAutoMargin = false
425
+ _hasPosition = false
426
+ // Same pattern for the 3× resolveEdges4Into calls at the top of every
427
+ // layoutNode(). In the 1000-node bench ~67% of those calls operate on
428
+ // all-undefined edge arrays (most nodes have no border; only cols have
429
+ // padding; only leaf cells have margin) — a single-branch skip beats
430
+ // ~20 property reads + ~15 compares + 4 writes of zeros.
431
+ _hasPadding = false
432
+ _hasBorder = false
433
+ _hasMargin = false
434
+ // -- Dirty-flag layout cache. Mirrors upstream CalculateLayout.cpp's
435
+ // layoutNodeInternal: skip a subtree entirely when it's clean and we're
436
+ // asking the same question we cached the answer to. Two slots since
437
+ // each node typically sees a measure call (performLayout=false, from
438
+ // computeFlexBasis) followed by a layout call (performLayout=true) with
439
+ // different inputs per parent pass — a single slot thrashes. Re-layout
440
+ // bench (dirty one leaf, recompute root) went 2.7x→1.1x with this:
441
+ // clean siblings skip straight through, only the dirty chain recomputes.
442
+ _lW = NaN
443
+ _lH = NaN
444
+ _lWM: MeasureMode = 0
445
+ _lHM: MeasureMode = 0
446
+ _lOW = NaN
447
+ _lOH = NaN
448
+ _lFW = false
449
+ _lFH = false
450
+ // _hasL stores INPUTS early (before compute) but layout.width/height are
451
+ // mutated by the multi-entry cache and by subsequent compute calls with
452
+ // different inputs. Without storing OUTPUTS, a _hasL hit returns whatever
453
+ // layout.width/height happened to be left by the last call — the scrollbox
454
+ // vpH=33→2624 bug. Store + restore outputs like the multi-entry cache does.
455
+ _lOutW = NaN
456
+ _lOutH = NaN
457
+ _hasL = false
458
+ _mW = NaN
459
+ _mH = NaN
460
+ _mWM: MeasureMode = 0
461
+ _mHM: MeasureMode = 0
462
+ _mOW = NaN
463
+ _mOH = NaN
464
+ _mOutW = NaN
465
+ _mOutH = NaN
466
+ _hasM = false
467
+ // Cached computeFlexBasis result. For clean children, basis only depends
468
+ // on the container's inner dimensions — if those haven't changed, skip the
469
+ // layoutNode(performLayout=false) recursion entirely. This is the hot path
470
+ // for scroll: 500-message content container is dirty, its 499 clean
471
+ // children each get measured ~20× as the dirty chain's measure/layout
472
+ // passes cascade. Basis cache short-circuits at the child boundary.
473
+ _fbBasis = NaN
474
+ _fbOwnerW = NaN
475
+ _fbOwnerH = NaN
476
+ _fbAvailMain = NaN
477
+ _fbAvailCross = NaN
478
+ _fbCrossMode: MeasureMode = 0
479
+ // Generation at which _fbBasis was written. Dirty nodes from a PREVIOUS
480
+ // generation have stale cache (subtree changed), but within the SAME
481
+ // generation the cache is fresh — the dirty chain's measure→layout
482
+ // cascade invokes computeFlexBasis ≥2^depth times per calculateLayout on
483
+ // fresh-mounted items, and the subtree doesn't change between calls.
484
+ // Gating on generation instead of isDirty_ lets fresh mounts (virtual
485
+ // scroll) cache-hit after first compute: 105k visits → ~10k.
486
+ _fbGen = -1
487
+ // Multi-entry layout cache — stores (inputs → computed w,h) so hits with
488
+ // different inputs than _hasL can restore the right dimensions. Upstream
489
+ // yoga uses 16; 4 covers Ink's dirty-chain depth. Packed as flat arrays
490
+ // to avoid per-entry object allocs. Slot i uses indices [i*8, i*8+8) in
491
+ // _cIn (aW,aH,wM,hM,oW,oH,fW,fH) and [i*2, i*2+2) in _cOut (w,h).
492
+ _cIn: Float64Array | null = null
493
+ _cOut: Float64Array | null = null
494
+ _cGen = -1
495
+ _cN = 0
496
+ _cWr = 0
497
+
498
+ constructor(config?: Config) {
499
+ this.style = defaultStyle()
500
+ this.layout = {
501
+ left: 0,
502
+ top: 0,
503
+ width: 0,
504
+ height: 0,
505
+ border: [0, 0, 0, 0],
506
+ padding: [0, 0, 0, 0],
507
+ margin: [0, 0, 0, 0],
508
+ }
509
+ this.parent = null
510
+ this.children = []
511
+ this.measureFunc = null
512
+ this.config = config ?? DEFAULT_CONFIG
513
+ this.isDirty_ = true
514
+ this.isReferenceBaseline_ = false
515
+ _yogaLiveNodes++
516
+ }
517
+
518
+ // -- Tree
519
+
520
+ insertChild(child: Node, index: number): void {
521
+ child.parent = this
522
+ this.children.splice(index, 0, child)
523
+ this.markDirty()
524
+ }
525
+ removeChild(child: Node): void {
526
+ const idx = this.children.indexOf(child)
527
+ if (idx >= 0) {
528
+ this.children.splice(idx, 1)
529
+ child.parent = null
530
+ this.markDirty()
531
+ }
532
+ }
533
+ getChild(index: number): Node {
534
+ return this.children[index]!
535
+ }
536
+ getChildCount(): number {
537
+ return this.children.length
538
+ }
539
+ getParent(): Node | null {
540
+ return this.parent
541
+ }
542
+
543
+ // -- Lifecycle
544
+
545
+ free(): void {
546
+ this.parent = null
547
+ this.children = []
548
+ this.measureFunc = null
549
+ this._cIn = null
550
+ this._cOut = null
551
+ _yogaLiveNodes--
552
+ }
553
+ freeRecursive(): void {
554
+ for (const c of this.children) c.freeRecursive()
555
+ this.free()
556
+ }
557
+ reset(): void {
558
+ this.style = defaultStyle()
559
+ this.children = []
560
+ this.parent = null
561
+ this.measureFunc = null
562
+ this.isDirty_ = true
563
+ this._hasAutoMargin = false
564
+ this._hasPosition = false
565
+ this._hasPadding = false
566
+ this._hasBorder = false
567
+ this._hasMargin = false
568
+ this._hasL = false
569
+ this._hasM = false
570
+ this._cN = 0
571
+ this._cWr = 0
572
+ this._fbBasis = NaN
573
+ }
574
+
575
+ // -- Dirty tracking
576
+
577
+ markDirty(): void {
578
+ this.isDirty_ = true
579
+ if (this.parent && !this.parent.isDirty_) this.parent.markDirty()
580
+ }
581
+ isDirty(): boolean {
582
+ return this.isDirty_
583
+ }
584
+ hasNewLayout(): boolean {
585
+ return true
586
+ }
587
+ markLayoutSeen(): void {}
588
+
589
+ // -- Measure function
590
+
591
+ setMeasureFunc(fn: MeasureFunction | null): void {
592
+ this.measureFunc = fn
593
+ this.markDirty()
594
+ }
595
+ unsetMeasureFunc(): void {
596
+ this.measureFunc = null
597
+ this.markDirty()
598
+ }
599
+
600
+ // -- Computed layout getters
601
+
602
+ getComputedLeft(): number {
603
+ return this.layout.left
604
+ }
605
+ getComputedTop(): number {
606
+ return this.layout.top
607
+ }
608
+ getComputedWidth(): number {
609
+ return this.layout.width
610
+ }
611
+ getComputedHeight(): number {
612
+ return this.layout.height
613
+ }
614
+ getComputedRight(): number {
615
+ const p = this.parent
616
+ return p ? p.layout.width - this.layout.left - this.layout.width : 0
617
+ }
618
+ getComputedBottom(): number {
619
+ const p = this.parent
620
+ return p ? p.layout.height - this.layout.top - this.layout.height : 0
621
+ }
622
+ getComputedLayout(): {
623
+ left: number
624
+ top: number
625
+ right: number
626
+ bottom: number
627
+ width: number
628
+ height: number
629
+ } {
630
+ return {
631
+ left: this.layout.left,
632
+ top: this.layout.top,
633
+ right: this.getComputedRight(),
634
+ bottom: this.getComputedBottom(),
635
+ width: this.layout.width,
636
+ height: this.layout.height,
637
+ }
638
+ }
639
+ getComputedBorder(edge: Edge): number {
640
+ return this.layout.border[physicalEdge(edge)]!
641
+ }
642
+ getComputedPadding(edge: Edge): number {
643
+ return this.layout.padding[physicalEdge(edge)]!
644
+ }
645
+ getComputedMargin(edge: Edge): number {
646
+ return this.layout.margin[physicalEdge(edge)]!
647
+ }
648
+
649
+ // -- Style setters: dimensions
650
+
651
+ setWidth(v: number | 'auto' | string | undefined): void {
652
+ this.style.width = parseDimension(v)
653
+ this.markDirty()
654
+ }
655
+ setWidthPercent(v: number): void {
656
+ this.style.width = percentValue(v)
657
+ this.markDirty()
658
+ }
659
+ setWidthAuto(): void {
660
+ this.style.width = AUTO_VALUE
661
+ this.markDirty()
662
+ }
663
+ setHeight(v: number | 'auto' | string | undefined): void {
664
+ this.style.height = parseDimension(v)
665
+ this.markDirty()
666
+ }
667
+ setHeightPercent(v: number): void {
668
+ this.style.height = percentValue(v)
669
+ this.markDirty()
670
+ }
671
+ setHeightAuto(): void {
672
+ this.style.height = AUTO_VALUE
673
+ this.markDirty()
674
+ }
675
+ setMinWidth(v: number | string | undefined): void {
676
+ this.style.minWidth = parseDimension(v)
677
+ this.markDirty()
678
+ }
679
+ setMinWidthPercent(v: number): void {
680
+ this.style.minWidth = percentValue(v)
681
+ this.markDirty()
682
+ }
683
+ setMinHeight(v: number | string | undefined): void {
684
+ this.style.minHeight = parseDimension(v)
685
+ this.markDirty()
686
+ }
687
+ setMinHeightPercent(v: number): void {
688
+ this.style.minHeight = percentValue(v)
689
+ this.markDirty()
690
+ }
691
+ setMaxWidth(v: number | string | undefined): void {
692
+ this.style.maxWidth = parseDimension(v)
693
+ this.markDirty()
694
+ }
695
+ setMaxWidthPercent(v: number): void {
696
+ this.style.maxWidth = percentValue(v)
697
+ this.markDirty()
698
+ }
699
+ setMaxHeight(v: number | string | undefined): void {
700
+ this.style.maxHeight = parseDimension(v)
701
+ this.markDirty()
702
+ }
703
+ setMaxHeightPercent(v: number): void {
704
+ this.style.maxHeight = percentValue(v)
705
+ this.markDirty()
706
+ }
707
+
708
+ // -- Style setters: flex
709
+
710
+ setFlexDirection(dir: FlexDirection): void {
711
+ this.style.flexDirection = dir
712
+ this.markDirty()
713
+ }
714
+ setFlexGrow(v: number | undefined): void {
715
+ this.style.flexGrow = v ?? 0
716
+ this.markDirty()
717
+ }
718
+ setFlexShrink(v: number | undefined): void {
719
+ this.style.flexShrink = v ?? 0
720
+ this.markDirty()
721
+ }
722
+ setFlex(v: number | undefined): void {
723
+ if (v === undefined || isNaN(v)) {
724
+ this.style.flexGrow = 0
725
+ this.style.flexShrink = 0
726
+ } else if (v > 0) {
727
+ this.style.flexGrow = v
728
+ this.style.flexShrink = 1
729
+ this.style.flexBasis = pointValue(0)
730
+ } else if (v < 0) {
731
+ this.style.flexGrow = 0
732
+ this.style.flexShrink = -v
733
+ } else {
734
+ this.style.flexGrow = 0
735
+ this.style.flexShrink = 0
736
+ }
737
+ this.markDirty()
738
+ }
739
+ setFlexBasis(v: number | 'auto' | string | undefined): void {
740
+ this.style.flexBasis = parseDimension(v)
741
+ this.markDirty()
742
+ }
743
+ setFlexBasisPercent(v: number): void {
744
+ this.style.flexBasis = percentValue(v)
745
+ this.markDirty()
746
+ }
747
+ setFlexBasisAuto(): void {
748
+ this.style.flexBasis = AUTO_VALUE
749
+ this.markDirty()
750
+ }
751
+ setFlexWrap(wrap: Wrap): void {
752
+ this.style.flexWrap = wrap
753
+ this.markDirty()
754
+ }
755
+
756
+ // -- Style setters: alignment
757
+
758
+ setAlignItems(a: Align): void {
759
+ this.style.alignItems = a
760
+ this.markDirty()
761
+ }
762
+ setAlignSelf(a: Align): void {
763
+ this.style.alignSelf = a
764
+ this.markDirty()
765
+ }
766
+ setAlignContent(a: Align): void {
767
+ this.style.alignContent = a
768
+ this.markDirty()
769
+ }
770
+ setJustifyContent(j: Justify): void {
771
+ this.style.justifyContent = j
772
+ this.markDirty()
773
+ }
774
+
775
+ // -- Style setters: display / position / overflow
776
+
777
+ setDisplay(d: Display): void {
778
+ this.style.display = d
779
+ this.markDirty()
780
+ }
781
+ getDisplay(): Display {
782
+ return this.style.display
783
+ }
784
+ setPositionType(t: PositionType): void {
785
+ this.style.positionType = t
786
+ this.markDirty()
787
+ }
788
+ setPosition(edge: Edge, v: number | string | undefined): void {
789
+ this.style.position[edge] = parseDimension(v)
790
+ this._hasPosition = hasAnyDefinedEdge(this.style.position)
791
+ this.markDirty()
792
+ }
793
+ setPositionPercent(edge: Edge, v: number): void {
794
+ this.style.position[edge] = percentValue(v)
795
+ this._hasPosition = true
796
+ this.markDirty()
797
+ }
798
+ setPositionAuto(edge: Edge): void {
799
+ this.style.position[edge] = AUTO_VALUE
800
+ this._hasPosition = true
801
+ this.markDirty()
802
+ }
803
+ setOverflow(o: Overflow): void {
804
+ this.style.overflow = o
805
+ this.markDirty()
806
+ }
807
+ setDirection(d: Direction): void {
808
+ this.style.direction = d
809
+ this.markDirty()
810
+ }
811
+ setBoxSizing(_: BoxSizing): void {
812
+ // Not implemented — Ink doesn't use content-box
813
+ }
814
+
815
+ // -- Style setters: spacing
816
+
817
+ setMargin(edge: Edge, v: number | 'auto' | string | undefined): void {
818
+ const val = parseDimension(v)
819
+ this.style.margin[edge] = val
820
+ if (val.unit === Unit.Auto) this._hasAutoMargin = true
821
+ else this._hasAutoMargin = hasAnyAutoEdge(this.style.margin)
822
+ this._hasMargin =
823
+ this._hasAutoMargin || hasAnyDefinedEdge(this.style.margin)
824
+ this.markDirty()
825
+ }
826
+ setMarginPercent(edge: Edge, v: number): void {
827
+ this.style.margin[edge] = percentValue(v)
828
+ this._hasAutoMargin = hasAnyAutoEdge(this.style.margin)
829
+ this._hasMargin = true
830
+ this.markDirty()
831
+ }
832
+ setMarginAuto(edge: Edge): void {
833
+ this.style.margin[edge] = AUTO_VALUE
834
+ this._hasAutoMargin = true
835
+ this._hasMargin = true
836
+ this.markDirty()
837
+ }
838
+ setPadding(edge: Edge, v: number | string | undefined): void {
839
+ this.style.padding[edge] = parseDimension(v)
840
+ this._hasPadding = hasAnyDefinedEdge(this.style.padding)
841
+ this.markDirty()
842
+ }
843
+ setPaddingPercent(edge: Edge, v: number): void {
844
+ this.style.padding[edge] = percentValue(v)
845
+ this._hasPadding = true
846
+ this.markDirty()
847
+ }
848
+ setBorder(edge: Edge, v: number | undefined): void {
849
+ this.style.border[edge] = v === undefined ? UNDEFINED_VALUE : pointValue(v)
850
+ this._hasBorder = hasAnyDefinedEdge(this.style.border)
851
+ this.markDirty()
852
+ }
853
+ setGap(gutter: Gutter, v: number | string | undefined): void {
854
+ this.style.gap[gutter] = parseDimension(v)
855
+ this.markDirty()
856
+ }
857
+ setGapPercent(gutter: Gutter, v: number): void {
858
+ this.style.gap[gutter] = percentValue(v)
859
+ this.markDirty()
860
+ }
861
+
862
+ // -- Style getters (partial — only what tests need)
863
+
864
+ getFlexDirection(): FlexDirection {
865
+ return this.style.flexDirection
866
+ }
867
+ getJustifyContent(): Justify {
868
+ return this.style.justifyContent
869
+ }
870
+ getAlignItems(): Align {
871
+ return this.style.alignItems
872
+ }
873
+ getAlignSelf(): Align {
874
+ return this.style.alignSelf
875
+ }
876
+ getAlignContent(): Align {
877
+ return this.style.alignContent
878
+ }
879
+ getFlexGrow(): number {
880
+ return this.style.flexGrow
881
+ }
882
+ getFlexShrink(): number {
883
+ return this.style.flexShrink
884
+ }
885
+ getFlexBasis(): Value {
886
+ return this.style.flexBasis
887
+ }
888
+ getFlexWrap(): Wrap {
889
+ return this.style.flexWrap
890
+ }
891
+ getWidth(): Value {
892
+ return this.style.width
893
+ }
894
+ getHeight(): Value {
895
+ return this.style.height
896
+ }
897
+ getOverflow(): Overflow {
898
+ return this.style.overflow
899
+ }
900
+ getPositionType(): PositionType {
901
+ return this.style.positionType
902
+ }
903
+ getDirection(): Direction {
904
+ return this.style.direction
905
+ }
906
+
907
+ // -- Unused API stubs (present for API parity)
908
+
909
+ copyStyle(_: Node): void {}
910
+ setDirtiedFunc(_: unknown): void {}
911
+ unsetDirtiedFunc(): void {}
912
+ setIsReferenceBaseline(v: boolean): void {
913
+ this.isReferenceBaseline_ = v
914
+ this.markDirty()
915
+ }
916
+ isReferenceBaseline(): boolean {
917
+ return this.isReferenceBaseline_
918
+ }
919
+ setAspectRatio(_: number | undefined): void {}
920
+ getAspectRatio(): number {
921
+ return NaN
922
+ }
923
+ setAlwaysFormsContainingBlock(_: boolean): void {}
924
+
925
+ // -- Layout entry point
926
+
927
+ calculateLayout(
928
+ ownerWidth: number | undefined,
929
+ ownerHeight: number | undefined,
930
+ _direction?: Direction,
931
+ ): void {
932
+ _yogaNodesVisited = 0
933
+ _yogaMeasureCalls = 0
934
+ _yogaCacheHits = 0
935
+ _generation++
936
+ const w = ownerWidth === undefined ? NaN : ownerWidth
937
+ const h = ownerHeight === undefined ? NaN : ownerHeight
938
+ layoutNode(
939
+ this,
940
+ w,
941
+ h,
942
+ isDefined(w) ? MeasureMode.Exactly : MeasureMode.Undefined,
943
+ isDefined(h) ? MeasureMode.Exactly : MeasureMode.Undefined,
944
+ w,
945
+ h,
946
+ true,
947
+ )
948
+ // Root's own position = margin + position insets (yoga applies position
949
+ // to the root even without a parent container; this matters for rounding
950
+ // since the root's abs top/left seeds the pixel-grid walk).
951
+ const mar = this.layout.margin
952
+ const posL = resolveValue(
953
+ resolveEdgeRaw(this.style.position, EDGE_LEFT),
954
+ isDefined(w) ? w : 0,
955
+ )
956
+ const posT = resolveValue(
957
+ resolveEdgeRaw(this.style.position, EDGE_TOP),
958
+ isDefined(w) ? w : 0,
959
+ )
960
+ this.layout.left = mar[EDGE_LEFT] + (isDefined(posL) ? posL : 0)
961
+ this.layout.top = mar[EDGE_TOP] + (isDefined(posT) ? posT : 0)
962
+ roundLayout(this, this.config.pointScaleFactor, 0, 0)
963
+ }
964
+ }
965
+
966
+ const DEFAULT_CONFIG = createConfig()
967
+
968
+ const CACHE_SLOTS = 4
969
+ function cacheWrite(
970
+ node: Node,
971
+ aW: number,
972
+ aH: number,
973
+ wM: MeasureMode,
974
+ hM: MeasureMode,
975
+ oW: number,
976
+ oH: number,
977
+ fW: boolean,
978
+ fH: boolean,
979
+ wasDirty: boolean,
980
+ ): void {
981
+ if (!node._cIn) {
982
+ node._cIn = new Float64Array(CACHE_SLOTS * 8)
983
+ node._cOut = new Float64Array(CACHE_SLOTS * 2)
984
+ }
985
+ // First write after a dirty clears stale entries from before the dirty.
986
+ // _cGen < _generation means entries are from a previous calculateLayout;
987
+ // if wasDirty, the subtree changed since then → old dimensions invalid.
988
+ // Clean nodes' old entries stay — same subtree → same result for same
989
+ // inputs, so cross-generation caching works (the scroll hot path where
990
+ // 499 clean messages cache-hit while one dirty leaf recomputes).
991
+ if (wasDirty && node._cGen !== _generation) {
992
+ node._cN = 0
993
+ node._cWr = 0
994
+ }
995
+ // LRU write index wraps; _cN stays at CACHE_SLOTS so the read scan always
996
+ // checks all populated slots (not just those since last wrap).
997
+ const i = node._cWr++ % CACHE_SLOTS
998
+ if (node._cN < CACHE_SLOTS) node._cN = node._cWr
999
+ const o = i * 8
1000
+ const cIn = node._cIn
1001
+ cIn[o] = aW
1002
+ cIn[o + 1] = aH
1003
+ cIn[o + 2] = wM
1004
+ cIn[o + 3] = hM
1005
+ cIn[o + 4] = oW
1006
+ cIn[o + 5] = oH
1007
+ cIn[o + 6] = fW ? 1 : 0
1008
+ cIn[o + 7] = fH ? 1 : 0
1009
+ node._cOut![i * 2] = node.layout.width
1010
+ node._cOut![i * 2 + 1] = node.layout.height
1011
+ node._cGen = _generation
1012
+ }
1013
+
1014
+ // Store computed layout.width/height into the single-slot cache output fields.
1015
+ // _hasL/_hasM inputs are committed at the TOP of layoutNode (before compute);
1016
+ // outputs must be committed HERE (after compute) so a cache hit can restore
1017
+ // the correct dimensions. Without this, a _hasL hit returns whatever
1018
+ // layout.width/height was left by the last call — which may be the intrinsic
1019
+ // content height from a heightMode=Undefined measure pass rather than the
1020
+ // constrained viewport height from the layout pass. That's the scrollbox
1021
+ // vpH=33→2624 bug: scrollTop clamps to 0, viewport goes blank.
1022
+ function commitCacheOutputs(node: Node, performLayout: boolean): void {
1023
+ if (performLayout) {
1024
+ node._lOutW = node.layout.width
1025
+ node._lOutH = node.layout.height
1026
+ } else {
1027
+ node._mOutW = node.layout.width
1028
+ node._mOutH = node.layout.height
1029
+ }
1030
+ }
1031
+
1032
+ // --
1033
+ // Core flexbox algorithm
1034
+
1035
+ // Profiling counters — reset per calculateLayout, read via getYogaCounters.
1036
+ // Incremented on each calculateLayout(). Nodes stamp _fbGen/_cGen when
1037
+ // their cache is written; a cache entry with gen === _generation was
1038
+ // computed THIS pass and is fresh regardless of isDirty_ state.
1039
+ let _generation = 0
1040
+ let _yogaNodesVisited = 0
1041
+ let _yogaMeasureCalls = 0
1042
+ let _yogaCacheHits = 0
1043
+ let _yogaLiveNodes = 0
1044
+ export function getYogaCounters(): {
1045
+ visited: number
1046
+ measured: number
1047
+ cacheHits: number
1048
+ live: number
1049
+ } {
1050
+ return {
1051
+ visited: _yogaNodesVisited,
1052
+ measured: _yogaMeasureCalls,
1053
+ cacheHits: _yogaCacheHits,
1054
+ live: _yogaLiveNodes,
1055
+ }
1056
+ }
1057
+
1058
+ function layoutNode(
1059
+ node: Node,
1060
+ availableWidth: number,
1061
+ availableHeight: number,
1062
+ widthMode: MeasureMode,
1063
+ heightMode: MeasureMode,
1064
+ ownerWidth: number,
1065
+ ownerHeight: number,
1066
+ performLayout: boolean,
1067
+ // When true, ignore style dimension on this axis — the flex container
1068
+ // has already determined the main size (flex-basis + grow/shrink result).
1069
+ forceWidth = false,
1070
+ forceHeight = false,
1071
+ ): void {
1072
+ _yogaNodesVisited++
1073
+ const style = node.style
1074
+ const layout = node.layout
1075
+
1076
+ // Dirty-flag skip: clean subtree + matching inputs → layout object already
1077
+ // holds the answer. A cached layout result also satisfies a measure request
1078
+ // (positions are a superset of dimensions); the reverse does not hold.
1079
+ // Same-generation entries are fresh regardless of isDirty_ — they were
1080
+ // computed THIS calculateLayout, the subtree hasn't changed since.
1081
+ // Previous-generation entries need !isDirty_ (a dirty node's cache from
1082
+ // before the dirty is stale).
1083
+ // sameGen bypass only for MEASURE calls — a layout-pass cache hit would
1084
+ // skip the child-positioning recursion (STEP 5), leaving children at
1085
+ // stale positions. Measure calls only need w/h which the cache stores.
1086
+ const sameGen = node._cGen === _generation && !performLayout
1087
+ if (!node.isDirty_ || sameGen) {
1088
+ if (
1089
+ !node.isDirty_ &&
1090
+ node._hasL &&
1091
+ node._lWM === widthMode &&
1092
+ node._lHM === heightMode &&
1093
+ node._lFW === forceWidth &&
1094
+ node._lFH === forceHeight &&
1095
+ sameFloat(node._lW, availableWidth) &&
1096
+ sameFloat(node._lH, availableHeight) &&
1097
+ sameFloat(node._lOW, ownerWidth) &&
1098
+ sameFloat(node._lOH, ownerHeight)
1099
+ ) {
1100
+ _yogaCacheHits++
1101
+ layout.width = node._lOutW
1102
+ layout.height = node._lOutH
1103
+ return
1104
+ }
1105
+ // Multi-entry cache: scan for matching inputs, restore cached w/h on hit.
1106
+ // Covers the scroll case where a dirty ancestor's measure→layout cascade
1107
+ // produces N>1 distinct input combos per clean child — the single _hasL
1108
+ // slot thrashed, forcing full subtree recursion. With 500-message
1109
+ // scrollbox and one dirty leaf, this took dirty-leaf relayout from
1110
+ // 76k layoutNode calls (21.7×nodes) to 4k (1.2×nodes), 6.86ms → 550µs.
1111
+ // Same-generation check covers fresh-mounted (dirty) nodes during
1112
+ // virtual scroll — the dirty chain invokes them ≥2^depth times, first
1113
+ // call writes cache, rest hit: 105k visits → ~10k for 1593-node tree.
1114
+ if (node._cN > 0 && (sameGen || !node.isDirty_)) {
1115
+ const cIn = node._cIn!
1116
+ for (let i = 0; i < node._cN; i++) {
1117
+ const o = i * 8
1118
+ if (
1119
+ cIn[o + 2] === widthMode &&
1120
+ cIn[o + 3] === heightMode &&
1121
+ cIn[o + 6] === (forceWidth ? 1 : 0) &&
1122
+ cIn[o + 7] === (forceHeight ? 1 : 0) &&
1123
+ sameFloat(cIn[o]!, availableWidth) &&
1124
+ sameFloat(cIn[o + 1]!, availableHeight) &&
1125
+ sameFloat(cIn[o + 4]!, ownerWidth) &&
1126
+ sameFloat(cIn[o + 5]!, ownerHeight)
1127
+ ) {
1128
+ layout.width = node._cOut![i * 2]!
1129
+ layout.height = node._cOut![i * 2 + 1]!
1130
+ _yogaCacheHits++
1131
+ return
1132
+ }
1133
+ }
1134
+ }
1135
+ if (
1136
+ !node.isDirty_ &&
1137
+ !performLayout &&
1138
+ node._hasM &&
1139
+ node._mWM === widthMode &&
1140
+ node._mHM === heightMode &&
1141
+ sameFloat(node._mW, availableWidth) &&
1142
+ sameFloat(node._mH, availableHeight) &&
1143
+ sameFloat(node._mOW, ownerWidth) &&
1144
+ sameFloat(node._mOH, ownerHeight)
1145
+ ) {
1146
+ layout.width = node._mOutW
1147
+ layout.height = node._mOutH
1148
+ _yogaCacheHits++
1149
+ return
1150
+ }
1151
+ }
1152
+ // Commit cache inputs up front so every return path leaves a valid entry.
1153
+ // Only clear isDirty_ on the LAYOUT pass — the measure pass (computeFlexBasis
1154
+ // → layoutNode(performLayout=false)) runs before the layout pass in the same
1155
+ // calculateLayout call. Clearing dirty during measure lets the subsequent
1156
+ // layout pass hit the STALE _hasL cache from the previous calculateLayout
1157
+ // (before children were inserted), so ScrollBox content height never grows
1158
+ // and sticky-scroll never follows new content. A dirty node's _hasL entry is
1159
+ // stale by definition — invalidate it so the layout pass recomputes.
1160
+ const wasDirty = node.isDirty_
1161
+ if (performLayout) {
1162
+ node._lW = availableWidth
1163
+ node._lH = availableHeight
1164
+ node._lWM = widthMode
1165
+ node._lHM = heightMode
1166
+ node._lOW = ownerWidth
1167
+ node._lOH = ownerHeight
1168
+ node._lFW = forceWidth
1169
+ node._lFH = forceHeight
1170
+ node._hasL = true
1171
+ node.isDirty_ = false
1172
+ // Previous approach cleared _cN here to prevent stale pre-dirty entries
1173
+ // from hitting (long-continuous blank-screen bug). Now replaced by
1174
+ // generation stamping: the cache check requires sameGen || !isDirty_, so
1175
+ // previous-generation entries from a dirty node can't hit. Clearing here
1176
+ // would wipe fresh same-generation entries from an earlier measure call,
1177
+ // forcing recompute on the layout call.
1178
+ if (wasDirty) node._hasM = false
1179
+ } else {
1180
+ node._mW = availableWidth
1181
+ node._mH = availableHeight
1182
+ node._mWM = widthMode
1183
+ node._mHM = heightMode
1184
+ node._mOW = ownerWidth
1185
+ node._mOH = ownerHeight
1186
+ node._hasM = true
1187
+ // Don't clear isDirty_. For DIRTY nodes, invalidate _hasL so the upcoming
1188
+ // performLayout=true call recomputes with the new child set (otherwise
1189
+ // sticky-scroll never follows new content — the bug from 4557bc9f9c).
1190
+ // Clean nodes keep _hasL: their layout from the previous generation is
1191
+ // still valid, they're only here because an ancestor is dirty and called
1192
+ // with different inputs than cached.
1193
+ if (wasDirty) node._hasL = false
1194
+ }
1195
+
1196
+ // Resolve padding/border/margin against ownerWidth (yoga uses ownerWidth for %)
1197
+ // Write directly into the pre-allocated layout arrays — avoids 3 allocs per
1198
+ // layoutNode call and 12 resolveEdge calls (was the #1 hotspot per CPU profile).
1199
+ // Skip entirely when no edges are set — the 4-write zero is cheaper than
1200
+ // the ~20 reads + ~15 compares resolveEdges4Into does to produce zeros.
1201
+ const pad = layout.padding
1202
+ const bor = layout.border
1203
+ const mar = layout.margin
1204
+ if (node._hasPadding) resolveEdges4Into(style.padding, ownerWidth, pad)
1205
+ else pad[0] = pad[1] = pad[2] = pad[3] = 0
1206
+ if (node._hasBorder) resolveEdges4Into(style.border, ownerWidth, bor)
1207
+ else bor[0] = bor[1] = bor[2] = bor[3] = 0
1208
+ if (node._hasMargin) resolveEdges4Into(style.margin, ownerWidth, mar)
1209
+ else mar[0] = mar[1] = mar[2] = mar[3] = 0
1210
+
1211
+ const paddingBorderWidth = pad[0] + pad[2] + bor[0] + bor[2]
1212
+ const paddingBorderHeight = pad[1] + pad[3] + bor[1] + bor[3]
1213
+
1214
+ // Resolve style dimensions
1215
+ const styleWidth = forceWidth ? NaN : resolveValue(style.width, ownerWidth)
1216
+ const styleHeight = forceHeight
1217
+ ? NaN
1218
+ : resolveValue(style.height, ownerHeight)
1219
+
1220
+ // If style dimension is defined, it overrides the available size
1221
+ let width = availableWidth
1222
+ let height = availableHeight
1223
+ let wMode = widthMode
1224
+ let hMode = heightMode
1225
+ if (isDefined(styleWidth)) {
1226
+ width = styleWidth
1227
+ wMode = MeasureMode.Exactly
1228
+ }
1229
+ if (isDefined(styleHeight)) {
1230
+ height = styleHeight
1231
+ hMode = MeasureMode.Exactly
1232
+ }
1233
+
1234
+ // Apply min/max constraints to the node's own dimensions
1235
+ width = boundAxis(style, true, width, ownerWidth, ownerHeight)
1236
+ height = boundAxis(style, false, height, ownerWidth, ownerHeight)
1237
+
1238
+ // Measure-func leaf node
1239
+ if (node.measureFunc && node.children.length === 0) {
1240
+ const innerW =
1241
+ wMode === MeasureMode.Undefined
1242
+ ? NaN
1243
+ : Math.max(0, width - paddingBorderWidth)
1244
+ const innerH =
1245
+ hMode === MeasureMode.Undefined
1246
+ ? NaN
1247
+ : Math.max(0, height - paddingBorderHeight)
1248
+ _yogaMeasureCalls++
1249
+ const measured = node.measureFunc(innerW, wMode, innerH, hMode)
1250
+ node.layout.width =
1251
+ wMode === MeasureMode.Exactly
1252
+ ? width
1253
+ : boundAxis(
1254
+ style,
1255
+ true,
1256
+ (measured.width ?? 0) + paddingBorderWidth,
1257
+ ownerWidth,
1258
+ ownerHeight,
1259
+ )
1260
+ node.layout.height =
1261
+ hMode === MeasureMode.Exactly
1262
+ ? height
1263
+ : boundAxis(
1264
+ style,
1265
+ false,
1266
+ (measured.height ?? 0) + paddingBorderHeight,
1267
+ ownerWidth,
1268
+ ownerHeight,
1269
+ )
1270
+ commitCacheOutputs(node, performLayout)
1271
+ // Write cache even for dirty nodes — fresh-mounted items during virtual
1272
+ // scroll are dirty on first layout, but the dirty chain's measure→layout
1273
+ // cascade invokes them ≥2^depth times per calculateLayout. Writing here
1274
+ // lets the 2nd+ calls hit cache (isDirty_ was cleared in the layout pass
1275
+ // above). Measured: 105k visits → 10k for a 1593-node fresh-mount tree.
1276
+ cacheWrite(
1277
+ node,
1278
+ availableWidth,
1279
+ availableHeight,
1280
+ widthMode,
1281
+ heightMode,
1282
+ ownerWidth,
1283
+ ownerHeight,
1284
+ forceWidth,
1285
+ forceHeight,
1286
+ wasDirty,
1287
+ )
1288
+ return
1289
+ }
1290
+
1291
+ // Leaf node with no children and no measure func
1292
+ if (node.children.length === 0) {
1293
+ node.layout.width =
1294
+ wMode === MeasureMode.Exactly
1295
+ ? width
1296
+ : boundAxis(style, true, paddingBorderWidth, ownerWidth, ownerHeight)
1297
+ node.layout.height =
1298
+ hMode === MeasureMode.Exactly
1299
+ ? height
1300
+ : boundAxis(style, false, paddingBorderHeight, ownerWidth, ownerHeight)
1301
+ commitCacheOutputs(node, performLayout)
1302
+ // Write cache even for dirty nodes — fresh-mounted items during virtual
1303
+ // scroll are dirty on first layout, but the dirty chain's measure→layout
1304
+ // cascade invokes them ≥2^depth times per calculateLayout. Writing here
1305
+ // lets the 2nd+ calls hit cache (isDirty_ was cleared in the layout pass
1306
+ // above). Measured: 105k visits → 10k for a 1593-node fresh-mount tree.
1307
+ cacheWrite(
1308
+ node,
1309
+ availableWidth,
1310
+ availableHeight,
1311
+ widthMode,
1312
+ heightMode,
1313
+ ownerWidth,
1314
+ ownerHeight,
1315
+ forceWidth,
1316
+ forceHeight,
1317
+ wasDirty,
1318
+ )
1319
+ return
1320
+ }
1321
+
1322
+ // Container with children — run flexbox algorithm
1323
+ const mainAxis = style.flexDirection
1324
+ const crossAx = crossAxis(mainAxis)
1325
+ const isMainRow = isRow(mainAxis)
1326
+
1327
+ const mainSize = isMainRow ? width : height
1328
+ const crossSize = isMainRow ? height : width
1329
+ const mainMode = isMainRow ? wMode : hMode
1330
+ const crossMode = isMainRow ? hMode : wMode
1331
+ const mainPadBorder = isMainRow ? paddingBorderWidth : paddingBorderHeight
1332
+ const crossPadBorder = isMainRow ? paddingBorderHeight : paddingBorderWidth
1333
+
1334
+ const innerMainSize = isDefined(mainSize)
1335
+ ? Math.max(0, mainSize - mainPadBorder)
1336
+ : NaN
1337
+ const innerCrossSize = isDefined(crossSize)
1338
+ ? Math.max(0, crossSize - crossPadBorder)
1339
+ : NaN
1340
+
1341
+ // Resolve gap
1342
+ const gapMain = resolveGap(
1343
+ style,
1344
+ isMainRow ? Gutter.Column : Gutter.Row,
1345
+ innerMainSize,
1346
+ )
1347
+
1348
+ // Partition children into flow vs absolute. display:contents nodes are
1349
+ // transparent — their children are lifted into the grandparent's child list
1350
+ // (recursively), and the contents node itself gets zero layout.
1351
+ const flowChildren: Node[] = []
1352
+ const absChildren: Node[] = []
1353
+ collectLayoutChildren(node, flowChildren, absChildren)
1354
+
1355
+ // ownerW/H are the reference sizes for resolving children's percentage
1356
+ // values. Per CSS, a % width resolves against the parent's content-box
1357
+ // width. If this node's width is indefinite, children's % widths are also
1358
+ // indefinite — do NOT fall through to the grandparent's size.
1359
+ const ownerW = isDefined(width) ? width : NaN
1360
+ const ownerH = isDefined(height) ? height : NaN
1361
+ const isWrap = style.flexWrap !== Wrap.NoWrap
1362
+ const gapCross = resolveGap(
1363
+ style,
1364
+ isMainRow ? Gutter.Row : Gutter.Column,
1365
+ innerCrossSize,
1366
+ )
1367
+
1368
+ // STEP 1: Compute flex-basis for each flow child and break into lines.
1369
+ // Single-line (NoWrap) containers always get one line; multi-line containers
1370
+ // break when accumulated basis+margin+gap exceeds innerMainSize.
1371
+ for (const c of flowChildren) {
1372
+ c._flexBasis = computeFlexBasis(
1373
+ c,
1374
+ mainAxis,
1375
+ innerMainSize,
1376
+ innerCrossSize,
1377
+ crossMode,
1378
+ ownerW,
1379
+ ownerH,
1380
+ )
1381
+ }
1382
+ const lines: Node[][] = []
1383
+ if (!isWrap || !isDefined(innerMainSize) || flowChildren.length === 0) {
1384
+ for (const c of flowChildren) c._lineIndex = 0
1385
+ lines.push(flowChildren)
1386
+ } else {
1387
+ // Line-break decisions use the min/max-clamped basis (flexbox spec §9.3.5:
1388
+ // "hypothetical main size"), not the raw flex-basis.
1389
+ let lineStart = 0
1390
+ let lineLen = 0
1391
+ for (let i = 0; i < flowChildren.length; i++) {
1392
+ const c = flowChildren[i]!
1393
+ const hypo = boundAxis(c.style, isMainRow, c._flexBasis, ownerW, ownerH)
1394
+ const outer = Math.max(0, hypo) + childMarginForAxis(c, mainAxis, ownerW)
1395
+ const withGap = i > lineStart ? gapMain : 0
1396
+ if (i > lineStart && lineLen + withGap + outer > innerMainSize) {
1397
+ lines.push(flowChildren.slice(lineStart, i))
1398
+ lineStart = i
1399
+ lineLen = outer
1400
+ } else {
1401
+ lineLen += withGap + outer
1402
+ }
1403
+ c._lineIndex = lines.length
1404
+ }
1405
+ lines.push(flowChildren.slice(lineStart))
1406
+ }
1407
+ const lineCount = lines.length
1408
+ const isBaseline = isBaselineLayout(node, flowChildren)
1409
+
1410
+ // STEP 2+3: For each line, resolve flexible lengths and lay out children to
1411
+ // measure cross sizes. Track per-line consumed main and max cross.
1412
+ const lineConsumedMain: number[] = new Array(lineCount)
1413
+ const lineCrossSizes: number[] = new Array(lineCount)
1414
+ // Baseline layout tracks max ascent (baseline + leading margin) per line so
1415
+ // baseline-aligned items can be positioned at maxAscent - childBaseline.
1416
+ const lineMaxAscent: number[] = isBaseline ? new Array(lineCount).fill(0) : []
1417
+ let maxLineMain = 0
1418
+ let totalLinesCross = 0
1419
+ for (let li = 0; li < lineCount; li++) {
1420
+ const line = lines[li]!
1421
+ const lineGap = line.length > 1 ? gapMain * (line.length - 1) : 0
1422
+ let lineBasis = lineGap
1423
+ for (const c of line) {
1424
+ lineBasis += c._flexBasis + childMarginForAxis(c, mainAxis, ownerW)
1425
+ }
1426
+ // Resolve flexible lengths against available inner main. For indefinite
1427
+ // containers with min/max, flex against the clamped size.
1428
+ let availMain = innerMainSize
1429
+ if (!isDefined(availMain)) {
1430
+ const mainOwner = isMainRow ? ownerWidth : ownerHeight
1431
+ const minM = resolveValue(
1432
+ isMainRow ? style.minWidth : style.minHeight,
1433
+ mainOwner,
1434
+ )
1435
+ const maxM = resolveValue(
1436
+ isMainRow ? style.maxWidth : style.maxHeight,
1437
+ mainOwner,
1438
+ )
1439
+ if (isDefined(maxM) && lineBasis > maxM - mainPadBorder) {
1440
+ availMain = Math.max(0, maxM - mainPadBorder)
1441
+ } else if (isDefined(minM) && lineBasis < minM - mainPadBorder) {
1442
+ availMain = Math.max(0, minM - mainPadBorder)
1443
+ }
1444
+ }
1445
+ resolveFlexibleLengths(
1446
+ line,
1447
+ availMain,
1448
+ lineBasis,
1449
+ isMainRow,
1450
+ ownerW,
1451
+ ownerH,
1452
+ )
1453
+
1454
+ // Lay out each child in this line to measure cross
1455
+ let lineCross = 0
1456
+ for (const c of line) {
1457
+ const cStyle = c.style
1458
+ const childAlign =
1459
+ cStyle.alignSelf === Align.Auto ? style.alignItems : cStyle.alignSelf
1460
+ const cMarginCross = childMarginForAxis(c, crossAx, ownerW)
1461
+ let childCrossSize = NaN
1462
+ let childCrossMode: MeasureMode = MeasureMode.Undefined
1463
+ const resolvedCrossStyle = resolveValue(
1464
+ isMainRow ? cStyle.height : cStyle.width,
1465
+ isMainRow ? ownerH : ownerW,
1466
+ )
1467
+ const crossLeadE = isMainRow ? EDGE_TOP : EDGE_LEFT
1468
+ const crossTrailE = isMainRow ? EDGE_BOTTOM : EDGE_RIGHT
1469
+ const hasCrossAutoMargin =
1470
+ c._hasAutoMargin &&
1471
+ (isMarginAuto(cStyle.margin, crossLeadE) ||
1472
+ isMarginAuto(cStyle.margin, crossTrailE))
1473
+ // Single-line stretch goes directly to the container cross size.
1474
+ // Multi-line wrap measures intrinsic cross (Undefined mode) so
1475
+ // flex-grow grandchildren don't expand to the container — the line
1476
+ // cross size is determined first, then items are re-stretched.
1477
+ if (isDefined(resolvedCrossStyle)) {
1478
+ childCrossSize = resolvedCrossStyle
1479
+ childCrossMode = MeasureMode.Exactly
1480
+ } else if (
1481
+ childAlign === Align.Stretch &&
1482
+ !hasCrossAutoMargin &&
1483
+ !isWrap &&
1484
+ isDefined(innerCrossSize) &&
1485
+ crossMode === MeasureMode.Exactly
1486
+ ) {
1487
+ childCrossSize = Math.max(0, innerCrossSize - cMarginCross)
1488
+ childCrossMode = MeasureMode.Exactly
1489
+ } else if (!isWrap && isDefined(innerCrossSize)) {
1490
+ childCrossSize = Math.max(0, innerCrossSize - cMarginCross)
1491
+ childCrossMode = MeasureMode.AtMost
1492
+ }
1493
+ const cw = isMainRow ? c._mainSize : childCrossSize
1494
+ const ch = isMainRow ? childCrossSize : c._mainSize
1495
+ layoutNode(
1496
+ c,
1497
+ cw,
1498
+ ch,
1499
+ isMainRow ? MeasureMode.Exactly : childCrossMode,
1500
+ isMainRow ? childCrossMode : MeasureMode.Exactly,
1501
+ ownerW,
1502
+ ownerH,
1503
+ performLayout,
1504
+ isMainRow,
1505
+ !isMainRow,
1506
+ )
1507
+ c._crossSize = isMainRow ? c.layout.height : c.layout.width
1508
+ lineCross = Math.max(lineCross, c._crossSize + cMarginCross)
1509
+ }
1510
+ // Baseline layout: line cross size must fit maxAscent + maxDescent of
1511
+ // baseline-aligned children (yoga STEP 8). Only applies to row direction.
1512
+ if (isBaseline) {
1513
+ let maxAscent = 0
1514
+ let maxDescent = 0
1515
+ for (const c of line) {
1516
+ if (resolveChildAlign(node, c) !== Align.Baseline) continue
1517
+ const mTop = resolveEdge(c.style.margin, EDGE_TOP, ownerW)
1518
+ const mBot = resolveEdge(c.style.margin, EDGE_BOTTOM, ownerW)
1519
+ const ascent = calculateBaseline(c) + mTop
1520
+ const descent = c.layout.height + mTop + mBot - ascent
1521
+ if (ascent > maxAscent) maxAscent = ascent
1522
+ if (descent > maxDescent) maxDescent = descent
1523
+ }
1524
+ lineMaxAscent[li] = maxAscent
1525
+ if (maxAscent + maxDescent > lineCross) {
1526
+ lineCross = maxAscent + maxDescent
1527
+ }
1528
+ }
1529
+ // layoutNode(c) at line ~1117 above already resolved c.layout.margin[] via
1530
+ // resolveEdges4Into with the same ownerW — read directly instead of
1531
+ // re-resolving through childMarginForAxis → 2× resolveEdge.
1532
+ const mainLead = leadingEdge(mainAxis)
1533
+ const mainTrail = trailingEdge(mainAxis)
1534
+ let consumed = lineGap
1535
+ for (const c of line) {
1536
+ const cm = c.layout.margin
1537
+ consumed += c._mainSize + cm[mainLead]! + cm[mainTrail]!
1538
+ }
1539
+ lineConsumedMain[li] = consumed
1540
+ lineCrossSizes[li] = lineCross
1541
+ maxLineMain = Math.max(maxLineMain, consumed)
1542
+ totalLinesCross += lineCross
1543
+ }
1544
+ const totalCrossGap = lineCount > 1 ? gapCross * (lineCount - 1) : 0
1545
+ totalLinesCross += totalCrossGap
1546
+
1547
+ // STEP 4: Determine container dimensions. Per yoga's STEP 9, for both
1548
+ // AtMost (FitContent) and Undefined (MaxContent) the node sizes to its
1549
+ // content — AtMost is NOT a hard clamp, items may overflow the available
1550
+ // space (CSS "fit-content" behavior). Only Scroll overflow clamps to the
1551
+ // available size. Wrap containers that broke into multiple lines under
1552
+ // AtMost fill the available main size since they wrapped at that boundary.
1553
+ const isScroll = style.overflow === Overflow.Scroll
1554
+ const contentMain = maxLineMain + mainPadBorder
1555
+ const finalMainSize =
1556
+ mainMode === MeasureMode.Exactly
1557
+ ? mainSize
1558
+ : mainMode === MeasureMode.AtMost && isScroll
1559
+ ? Math.max(Math.min(mainSize, contentMain), mainPadBorder)
1560
+ : isWrap && lineCount > 1 && mainMode === MeasureMode.AtMost
1561
+ ? mainSize
1562
+ : contentMain
1563
+ const contentCross = totalLinesCross + crossPadBorder
1564
+ const finalCrossSize =
1565
+ crossMode === MeasureMode.Exactly
1566
+ ? crossSize
1567
+ : crossMode === MeasureMode.AtMost && isScroll
1568
+ ? Math.max(Math.min(crossSize, contentCross), crossPadBorder)
1569
+ : contentCross
1570
+ node.layout.width = boundAxis(
1571
+ style,
1572
+ true,
1573
+ isMainRow ? finalMainSize : finalCrossSize,
1574
+ ownerWidth,
1575
+ ownerHeight,
1576
+ )
1577
+ node.layout.height = boundAxis(
1578
+ style,
1579
+ false,
1580
+ isMainRow ? finalCrossSize : finalMainSize,
1581
+ ownerWidth,
1582
+ ownerHeight,
1583
+ )
1584
+ commitCacheOutputs(node, performLayout)
1585
+ // Write cache even for dirty nodes — fresh-mounted items during virtual scroll
1586
+ cacheWrite(
1587
+ node,
1588
+ availableWidth,
1589
+ availableHeight,
1590
+ widthMode,
1591
+ heightMode,
1592
+ ownerWidth,
1593
+ ownerHeight,
1594
+ forceWidth,
1595
+ forceHeight,
1596
+ wasDirty,
1597
+ )
1598
+
1599
+ if (!performLayout) return
1600
+
1601
+ // STEP 5: Position lines (align-content) and children (justify-content +
1602
+ // align-items + auto margins).
1603
+ const actualInnerMain =
1604
+ (isMainRow ? node.layout.width : node.layout.height) - mainPadBorder
1605
+ const actualInnerCross =
1606
+ (isMainRow ? node.layout.height : node.layout.width) - crossPadBorder
1607
+ const mainLeadEdgePhys = leadingEdge(mainAxis)
1608
+ const mainTrailEdgePhys = trailingEdge(mainAxis)
1609
+ const crossLeadEdgePhys = isMainRow ? EDGE_TOP : EDGE_LEFT
1610
+ const crossTrailEdgePhys = isMainRow ? EDGE_BOTTOM : EDGE_RIGHT
1611
+ const reversed = isReverse(mainAxis)
1612
+ const mainContainerSize = isMainRow ? node.layout.width : node.layout.height
1613
+ const crossLead = pad[crossLeadEdgePhys]! + bor[crossLeadEdgePhys]!
1614
+
1615
+ // Align-content: distribute free cross space among lines. Single-line
1616
+ // containers use the full cross size for the one line (align-items handles
1617
+ // positioning within it).
1618
+ let lineCrossOffset = crossLead
1619
+ let betweenLines = gapCross
1620
+ const freeCross = actualInnerCross - totalLinesCross
1621
+ if (lineCount === 1 && !isWrap && !isBaseline) {
1622
+ lineCrossSizes[0] = actualInnerCross
1623
+ } else {
1624
+ const remCross = Math.max(0, freeCross)
1625
+ switch (style.alignContent) {
1626
+ case Align.FlexStart:
1627
+ break
1628
+ case Align.Center:
1629
+ lineCrossOffset += freeCross / 2
1630
+ break
1631
+ case Align.FlexEnd:
1632
+ lineCrossOffset += freeCross
1633
+ break
1634
+ case Align.Stretch:
1635
+ if (lineCount > 0 && remCross > 0) {
1636
+ const add = remCross / lineCount
1637
+ for (let i = 0; i < lineCount; i++) lineCrossSizes[i]! += add
1638
+ }
1639
+ break
1640
+ case Align.SpaceBetween:
1641
+ if (lineCount > 1) betweenLines += remCross / (lineCount - 1)
1642
+ break
1643
+ case Align.SpaceAround:
1644
+ if (lineCount > 0) {
1645
+ betweenLines += remCross / lineCount
1646
+ lineCrossOffset += remCross / lineCount / 2
1647
+ }
1648
+ break
1649
+ case Align.SpaceEvenly:
1650
+ if (lineCount > 0) {
1651
+ betweenLines += remCross / (lineCount + 1)
1652
+ lineCrossOffset += remCross / (lineCount + 1)
1653
+ }
1654
+ break
1655
+ default:
1656
+ break
1657
+ }
1658
+ }
1659
+
1660
+ // For wrap-reverse, lines stack from the trailing cross edge. Walk lines in
1661
+ // order but flip the cross position within the container.
1662
+ const wrapReverse = style.flexWrap === Wrap.WrapReverse
1663
+ const crossContainerSize = isMainRow ? node.layout.height : node.layout.width
1664
+ let lineCrossPos = lineCrossOffset
1665
+ for (let li = 0; li < lineCount; li++) {
1666
+ const line = lines[li]!
1667
+ const lineCross = lineCrossSizes[li]!
1668
+ const consumedMain = lineConsumedMain[li]!
1669
+ const n = line.length
1670
+
1671
+ // Re-stretch children whose cross is auto and align is stretch, now that
1672
+ // the line cross size is known. Needed for multi-line wrap (line cross
1673
+ // wasn't known during initial measure) AND single-line when the container
1674
+ // cross was not Exactly (initial stretch at ~line 1250 was skipped because
1675
+ // innerCrossSize wasn't defined — the container sized to max child cross).
1676
+ if (isWrap || crossMode !== MeasureMode.Exactly) {
1677
+ for (const c of line) {
1678
+ const cStyle = c.style
1679
+ const childAlign =
1680
+ cStyle.alignSelf === Align.Auto ? style.alignItems : cStyle.alignSelf
1681
+ const crossStyleDef = isDefined(
1682
+ resolveValue(
1683
+ isMainRow ? cStyle.height : cStyle.width,
1684
+ isMainRow ? ownerH : ownerW,
1685
+ ),
1686
+ )
1687
+ const hasCrossAutoMargin =
1688
+ c._hasAutoMargin &&
1689
+ (isMarginAuto(cStyle.margin, crossLeadEdgePhys) ||
1690
+ isMarginAuto(cStyle.margin, crossTrailEdgePhys))
1691
+ if (
1692
+ childAlign === Align.Stretch &&
1693
+ !crossStyleDef &&
1694
+ !hasCrossAutoMargin
1695
+ ) {
1696
+ const cMarginCross = childMarginForAxis(c, crossAx, ownerW)
1697
+ const target = Math.max(0, lineCross - cMarginCross)
1698
+ if (c._crossSize !== target) {
1699
+ const cw = isMainRow ? c._mainSize : target
1700
+ const ch = isMainRow ? target : c._mainSize
1701
+ layoutNode(
1702
+ c,
1703
+ cw,
1704
+ ch,
1705
+ MeasureMode.Exactly,
1706
+ MeasureMode.Exactly,
1707
+ ownerW,
1708
+ ownerH,
1709
+ performLayout,
1710
+ isMainRow,
1711
+ !isMainRow,
1712
+ )
1713
+ c._crossSize = target
1714
+ }
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ // Justify-content + auto margins for this line
1720
+ let mainOffset = pad[mainLeadEdgePhys]! + bor[mainLeadEdgePhys]!
1721
+ let betweenMain = gapMain
1722
+ let numAutoMarginsMain = 0
1723
+ for (const c of line) {
1724
+ if (!c._hasAutoMargin) continue
1725
+ if (isMarginAuto(c.style.margin, mainLeadEdgePhys)) numAutoMarginsMain++
1726
+ if (isMarginAuto(c.style.margin, mainTrailEdgePhys)) numAutoMarginsMain++
1727
+ }
1728
+ const freeMain = actualInnerMain - consumedMain
1729
+ const remainingMain = Math.max(0, freeMain)
1730
+ const autoMarginMainSize =
1731
+ numAutoMarginsMain > 0 && remainingMain > 0
1732
+ ? remainingMain / numAutoMarginsMain
1733
+ : 0
1734
+ if (numAutoMarginsMain === 0) {
1735
+ switch (style.justifyContent) {
1736
+ case Justify.FlexStart:
1737
+ break
1738
+ case Justify.Center:
1739
+ mainOffset += freeMain / 2
1740
+ break
1741
+ case Justify.FlexEnd:
1742
+ mainOffset += freeMain
1743
+ break
1744
+ case Justify.SpaceBetween:
1745
+ if (n > 1) betweenMain += remainingMain / (n - 1)
1746
+ break
1747
+ case Justify.SpaceAround:
1748
+ if (n > 0) {
1749
+ betweenMain += remainingMain / n
1750
+ mainOffset += remainingMain / n / 2
1751
+ }
1752
+ break
1753
+ case Justify.SpaceEvenly:
1754
+ if (n > 0) {
1755
+ betweenMain += remainingMain / (n + 1)
1756
+ mainOffset += remainingMain / (n + 1)
1757
+ }
1758
+ break
1759
+ }
1760
+ }
1761
+
1762
+ const effectiveLineCrossPos = wrapReverse
1763
+ ? crossContainerSize - lineCrossPos - lineCross
1764
+ : lineCrossPos
1765
+
1766
+ let pos = mainOffset
1767
+ for (const c of line) {
1768
+ const cMargin = c.style.margin
1769
+ // c.layout.margin[] was populated by resolveEdges4Into inside the
1770
+ // layoutNode(c) call above (same ownerW). Read resolved values directly
1771
+ // instead of re-running the edge fallback chain 4× via resolveEdge.
1772
+ // Auto margins resolve to 0 in layout.margin, so autoMarginMainSize
1773
+ // substitution still uses the isMarginAuto check against style.
1774
+ const cLayoutMargin = c.layout.margin
1775
+ let autoMainLead = false
1776
+ let autoMainTrail = false
1777
+ let autoCrossLead = false
1778
+ let autoCrossTrail = false
1779
+ let mMainLead: number
1780
+ let mMainTrail: number
1781
+ let mCrossLead: number
1782
+ let mCrossTrail: number
1783
+ if (c._hasAutoMargin) {
1784
+ autoMainLead = isMarginAuto(cMargin, mainLeadEdgePhys)
1785
+ autoMainTrail = isMarginAuto(cMargin, mainTrailEdgePhys)
1786
+ autoCrossLead = isMarginAuto(cMargin, crossLeadEdgePhys)
1787
+ autoCrossTrail = isMarginAuto(cMargin, crossTrailEdgePhys)
1788
+ mMainLead = autoMainLead
1789
+ ? autoMarginMainSize
1790
+ : cLayoutMargin[mainLeadEdgePhys]!
1791
+ mMainTrail = autoMainTrail
1792
+ ? autoMarginMainSize
1793
+ : cLayoutMargin[mainTrailEdgePhys]!
1794
+ mCrossLead = autoCrossLead ? 0 : cLayoutMargin[crossLeadEdgePhys]!
1795
+ mCrossTrail = autoCrossTrail ? 0 : cLayoutMargin[crossTrailEdgePhys]!
1796
+ } else {
1797
+ // Fast path: no auto margins — read resolved values directly.
1798
+ mMainLead = cLayoutMargin[mainLeadEdgePhys]!
1799
+ mMainTrail = cLayoutMargin[mainTrailEdgePhys]!
1800
+ mCrossLead = cLayoutMargin[crossLeadEdgePhys]!
1801
+ mCrossTrail = cLayoutMargin[crossTrailEdgePhys]!
1802
+ }
1803
+
1804
+ const mainPos = reversed
1805
+ ? mainContainerSize - (pos + mMainLead) - c._mainSize
1806
+ : pos + mMainLead
1807
+
1808
+ const childAlign =
1809
+ c.style.alignSelf === Align.Auto ? style.alignItems : c.style.alignSelf
1810
+ let crossPos = effectiveLineCrossPos + mCrossLead
1811
+ const crossFree = lineCross - c._crossSize - mCrossLead - mCrossTrail
1812
+ if (autoCrossLead && autoCrossTrail) {
1813
+ crossPos += Math.max(0, crossFree) / 2
1814
+ } else if (autoCrossLead) {
1815
+ crossPos += Math.max(0, crossFree)
1816
+ } else if (autoCrossTrail) {
1817
+ // stays at leading
1818
+ } else {
1819
+ switch (childAlign) {
1820
+ case Align.FlexStart:
1821
+ case Align.Stretch:
1822
+ if (wrapReverse) crossPos += crossFree
1823
+ break
1824
+ case Align.Center:
1825
+ crossPos += crossFree / 2
1826
+ break
1827
+ case Align.FlexEnd:
1828
+ if (!wrapReverse) crossPos += crossFree
1829
+ break
1830
+ case Align.Baseline:
1831
+ // Row direction only (isBaselineLayout checked this). Position so
1832
+ // the child's baseline aligns with the line's max ascent. Per
1833
+ // yoga: top = currentLead + maxAscent - childBaseline + leadingPosition.
1834
+ if (isBaseline) {
1835
+ crossPos =
1836
+ effectiveLineCrossPos +
1837
+ lineMaxAscent[li]! -
1838
+ calculateBaseline(c)
1839
+ }
1840
+ break
1841
+ default:
1842
+ break
1843
+ }
1844
+ }
1845
+
1846
+ // Relative position offsets. Fast path: no position insets set →
1847
+ // skip 4× resolveEdgeRaw + 4× resolveValue + 4× isDefined.
1848
+ let relX = 0
1849
+ let relY = 0
1850
+ if (c._hasPosition) {
1851
+ const relLeft = resolveValue(
1852
+ resolveEdgeRaw(c.style.position, EDGE_LEFT),
1853
+ ownerW,
1854
+ )
1855
+ const relRight = resolveValue(
1856
+ resolveEdgeRaw(c.style.position, EDGE_RIGHT),
1857
+ ownerW,
1858
+ )
1859
+ const relTop = resolveValue(
1860
+ resolveEdgeRaw(c.style.position, EDGE_TOP),
1861
+ ownerW,
1862
+ )
1863
+ const relBottom = resolveValue(
1864
+ resolveEdgeRaw(c.style.position, EDGE_BOTTOM),
1865
+ ownerW,
1866
+ )
1867
+ relX = isDefined(relLeft)
1868
+ ? relLeft
1869
+ : isDefined(relRight)
1870
+ ? -relRight
1871
+ : 0
1872
+ relY = isDefined(relTop)
1873
+ ? relTop
1874
+ : isDefined(relBottom)
1875
+ ? -relBottom
1876
+ : 0
1877
+ }
1878
+
1879
+ if (isMainRow) {
1880
+ c.layout.left = mainPos + relX
1881
+ c.layout.top = crossPos + relY
1882
+ } else {
1883
+ c.layout.left = crossPos + relX
1884
+ c.layout.top = mainPos + relY
1885
+ }
1886
+ pos += c._mainSize + mMainLead + mMainTrail + betweenMain
1887
+ }
1888
+ lineCrossPos += lineCross + betweenLines
1889
+ }
1890
+
1891
+ // STEP 6: Absolute-positioned children
1892
+ for (const c of absChildren) {
1893
+ layoutAbsoluteChild(
1894
+ node,
1895
+ c,
1896
+ node.layout.width,
1897
+ node.layout.height,
1898
+ pad,
1899
+ bor,
1900
+ )
1901
+ }
1902
+ }
1903
+
1904
+ function layoutAbsoluteChild(
1905
+ parent: Node,
1906
+ child: Node,
1907
+ parentWidth: number,
1908
+ parentHeight: number,
1909
+ pad: [number, number, number, number],
1910
+ bor: [number, number, number, number],
1911
+ ): void {
1912
+ const cs = child.style
1913
+ const posLeft = resolveEdgeRaw(cs.position, EDGE_LEFT)
1914
+ const posRight = resolveEdgeRaw(cs.position, EDGE_RIGHT)
1915
+ const posTop = resolveEdgeRaw(cs.position, EDGE_TOP)
1916
+ const posBottom = resolveEdgeRaw(cs.position, EDGE_BOTTOM)
1917
+
1918
+ const rLeft = resolveValue(posLeft, parentWidth)
1919
+ const rRight = resolveValue(posRight, parentWidth)
1920
+ const rTop = resolveValue(posTop, parentHeight)
1921
+ const rBottom = resolveValue(posBottom, parentHeight)
1922
+
1923
+ // Absolute children's percentage dimensions resolve against the containing
1924
+ // block's padding-box (parent size minus border), per CSS §10.1.
1925
+ const paddingBoxW = parentWidth - bor[0] - bor[2]
1926
+ const paddingBoxH = parentHeight - bor[1] - bor[3]
1927
+ let cw = resolveValue(cs.width, paddingBoxW)
1928
+ let ch = resolveValue(cs.height, paddingBoxH)
1929
+
1930
+ // If both left+right defined and width not, derive width
1931
+ if (!isDefined(cw) && isDefined(rLeft) && isDefined(rRight)) {
1932
+ cw = paddingBoxW - rLeft - rRight
1933
+ }
1934
+ if (!isDefined(ch) && isDefined(rTop) && isDefined(rBottom)) {
1935
+ ch = paddingBoxH - rTop - rBottom
1936
+ }
1937
+
1938
+ layoutNode(
1939
+ child,
1940
+ cw,
1941
+ ch,
1942
+ isDefined(cw) ? MeasureMode.Exactly : MeasureMode.Undefined,
1943
+ isDefined(ch) ? MeasureMode.Exactly : MeasureMode.Undefined,
1944
+ paddingBoxW,
1945
+ paddingBoxH,
1946
+ true,
1947
+ )
1948
+
1949
+ // Margin of absolute child (applied in addition to insets)
1950
+ const mL = resolveEdge(cs.margin, EDGE_LEFT, parentWidth)
1951
+ const mT = resolveEdge(cs.margin, EDGE_TOP, parentWidth)
1952
+ const mR = resolveEdge(cs.margin, EDGE_RIGHT, parentWidth)
1953
+ const mB = resolveEdge(cs.margin, EDGE_BOTTOM, parentWidth)
1954
+
1955
+ const mainAxis = parent.style.flexDirection
1956
+ const reversed = isReverse(mainAxis)
1957
+ const mainRow = isRow(mainAxis)
1958
+ const wrapReverse = parent.style.flexWrap === Wrap.WrapReverse
1959
+ // alignSelf overrides alignItems for absolute children (same as flow items)
1960
+ const alignment =
1961
+ cs.alignSelf === Align.Auto ? parent.style.alignItems : cs.alignSelf
1962
+
1963
+ // Position
1964
+ let left: number
1965
+ if (isDefined(rLeft)) {
1966
+ left = bor[0] + rLeft + mL
1967
+ } else if (isDefined(rRight)) {
1968
+ left = parentWidth - bor[2] - rRight - child.layout.width - mR
1969
+ } else if (mainRow) {
1970
+ // Main axis — justify-content, flipped for reversed
1971
+ const lead = pad[0] + bor[0]
1972
+ const trail = parentWidth - pad[2] - bor[2]
1973
+ left = reversed
1974
+ ? trail - child.layout.width - mR
1975
+ : justifyAbsolute(
1976
+ parent.style.justifyContent,
1977
+ lead,
1978
+ trail,
1979
+ child.layout.width,
1980
+ ) + mL
1981
+ } else {
1982
+ left =
1983
+ alignAbsolute(
1984
+ alignment,
1985
+ pad[0] + bor[0],
1986
+ parentWidth - pad[2] - bor[2],
1987
+ child.layout.width,
1988
+ wrapReverse,
1989
+ ) + mL
1990
+ }
1991
+
1992
+ let top: number
1993
+ if (isDefined(rTop)) {
1994
+ top = bor[1] + rTop + mT
1995
+ } else if (isDefined(rBottom)) {
1996
+ top = parentHeight - bor[3] - rBottom - child.layout.height - mB
1997
+ } else if (mainRow) {
1998
+ top =
1999
+ alignAbsolute(
2000
+ alignment,
2001
+ pad[1] + bor[1],
2002
+ parentHeight - pad[3] - bor[3],
2003
+ child.layout.height,
2004
+ wrapReverse,
2005
+ ) + mT
2006
+ } else {
2007
+ const lead = pad[1] + bor[1]
2008
+ const trail = parentHeight - pad[3] - bor[3]
2009
+ top = reversed
2010
+ ? trail - child.layout.height - mB
2011
+ : justifyAbsolute(
2012
+ parent.style.justifyContent,
2013
+ lead,
2014
+ trail,
2015
+ child.layout.height,
2016
+ ) + mT
2017
+ }
2018
+
2019
+ child.layout.left = left
2020
+ child.layout.top = top
2021
+ }
2022
+
2023
+ function justifyAbsolute(
2024
+ justify: Justify,
2025
+ leadEdge: number,
2026
+ trailEdge: number,
2027
+ childSize: number,
2028
+ ): number {
2029
+ switch (justify) {
2030
+ case Justify.Center:
2031
+ return leadEdge + (trailEdge - leadEdge - childSize) / 2
2032
+ case Justify.FlexEnd:
2033
+ return trailEdge - childSize
2034
+ default:
2035
+ return leadEdge
2036
+ }
2037
+ }
2038
+
2039
+ function alignAbsolute(
2040
+ align: Align,
2041
+ leadEdge: number,
2042
+ trailEdge: number,
2043
+ childSize: number,
2044
+ wrapReverse: boolean,
2045
+ ): number {
2046
+ // Wrap-reverse flips the cross axis: flex-start/stretch go to trailing,
2047
+ // flex-end goes to leading (yoga's absoluteLayoutChild flips the align value
2048
+ // when the containing block has wrap-reverse).
2049
+ switch (align) {
2050
+ case Align.Center:
2051
+ return leadEdge + (trailEdge - leadEdge - childSize) / 2
2052
+ case Align.FlexEnd:
2053
+ return wrapReverse ? leadEdge : trailEdge - childSize
2054
+ default:
2055
+ return wrapReverse ? trailEdge - childSize : leadEdge
2056
+ }
2057
+ }
2058
+
2059
+ function computeFlexBasis(
2060
+ child: Node,
2061
+ mainAxis: FlexDirection,
2062
+ availableMain: number,
2063
+ availableCross: number,
2064
+ crossMode: MeasureMode,
2065
+ ownerWidth: number,
2066
+ ownerHeight: number,
2067
+ ): number {
2068
+ // Same-generation cache hit: basis was computed THIS calculateLayout, so
2069
+ // it's fresh regardless of isDirty_. Covers both clean children (scrolling
2070
+ // past unchanged messages) AND fresh-mounted dirty children (virtual
2071
+ // scroll mounts new items — the dirty chain's measure→layout cascade
2072
+ // invokes this ≥2^depth times, but the child's subtree doesn't change
2073
+ // between calls within one calculateLayout). For clean children with
2074
+ // cache from a PREVIOUS generation, also hit if inputs match — isDirty_
2075
+ // gates since a dirty child's previous-gen cache is stale.
2076
+ const sameGen = child._fbGen === _generation
2077
+ if (
2078
+ (sameGen || !child.isDirty_) &&
2079
+ child._fbCrossMode === crossMode &&
2080
+ sameFloat(child._fbOwnerW, ownerWidth) &&
2081
+ sameFloat(child._fbOwnerH, ownerHeight) &&
2082
+ sameFloat(child._fbAvailMain, availableMain) &&
2083
+ sameFloat(child._fbAvailCross, availableCross)
2084
+ ) {
2085
+ return child._fbBasis
2086
+ }
2087
+ const cs = child.style
2088
+ const isMainRow = isRow(mainAxis)
2089
+
2090
+ // Explicit flex-basis
2091
+ const basis = resolveValue(cs.flexBasis, availableMain)
2092
+ if (isDefined(basis)) {
2093
+ const b = Math.max(0, basis)
2094
+ child._fbBasis = b
2095
+ child._fbOwnerW = ownerWidth
2096
+ child._fbOwnerH = ownerHeight
2097
+ child._fbAvailMain = availableMain
2098
+ child._fbAvailCross = availableCross
2099
+ child._fbCrossMode = crossMode
2100
+ child._fbGen = _generation
2101
+ return b
2102
+ }
2103
+
2104
+ // Style dimension on main axis
2105
+ const mainStyleDim = isMainRow ? cs.width : cs.height
2106
+ const mainOwner = isMainRow ? ownerWidth : ownerHeight
2107
+ const resolved = resolveValue(mainStyleDim, mainOwner)
2108
+ if (isDefined(resolved)) {
2109
+ const b = Math.max(0, resolved)
2110
+ child._fbBasis = b
2111
+ child._fbOwnerW = ownerWidth
2112
+ child._fbOwnerH = ownerHeight
2113
+ child._fbAvailMain = availableMain
2114
+ child._fbAvailCross = availableCross
2115
+ child._fbCrossMode = crossMode
2116
+ child._fbGen = _generation
2117
+ return b
2118
+ }
2119
+
2120
+ // Need to measure the child to get its natural size
2121
+ const crossStyleDim = isMainRow ? cs.height : cs.width
2122
+ const crossOwner = isMainRow ? ownerHeight : ownerWidth
2123
+ let crossConstraint = resolveValue(crossStyleDim, crossOwner)
2124
+ let crossConstraintMode: MeasureMode = isDefined(crossConstraint)
2125
+ ? MeasureMode.Exactly
2126
+ : MeasureMode.Undefined
2127
+ if (!isDefined(crossConstraint) && isDefined(availableCross)) {
2128
+ crossConstraint = availableCross
2129
+ crossConstraintMode =
2130
+ crossMode === MeasureMode.Exactly && isStretchAlign(child)
2131
+ ? MeasureMode.Exactly
2132
+ : MeasureMode.AtMost
2133
+ }
2134
+
2135
+ // Upstream yoga (YGNodeComputeFlexBasisForChild) passes the available inner
2136
+ // width with mode AtMost when the subtree will call a measure-func — so text
2137
+ // nodes don't report unconstrained intrinsic width as flex-basis, which
2138
+ // would force siblings to shrink and the text to wrap at the wrong width.
2139
+ // Passing Undefined here made Ink's <Text> inside <Box flexGrow={1}> get
2140
+ // width = intrinsic instead of available, dropping chars at wrap boundaries.
2141
+ //
2142
+ // Two constraints on when this applies:
2143
+ // - Width only. Height is never constrained during basis measurement —
2144
+ // column containers must measure children at natural height so
2145
+ // scrollable content can overflow (constraining height clips ScrollBox).
2146
+ // - Subtree has a measure-func. Pure layout subtrees (no measure-func)
2147
+ // with flex-grow children would grow into the AtMost constraint,
2148
+ // inflating the basis (breaks YGMinMaxDimensionTest flex_grow_in_at_most
2149
+ // where a flexGrow:1 child should stay at basis 0, not grow to 100).
2150
+ let mainConstraint = NaN
2151
+ let mainConstraintMode: MeasureMode = MeasureMode.Undefined
2152
+ if (isMainRow && isDefined(availableMain) && hasMeasureFuncInSubtree(child)) {
2153
+ mainConstraint = availableMain
2154
+ mainConstraintMode = MeasureMode.AtMost
2155
+ }
2156
+
2157
+ const mw = isMainRow ? mainConstraint : crossConstraint
2158
+ const mh = isMainRow ? crossConstraint : mainConstraint
2159
+ const mwMode = isMainRow ? mainConstraintMode : crossConstraintMode
2160
+ const mhMode = isMainRow ? crossConstraintMode : mainConstraintMode
2161
+
2162
+ layoutNode(child, mw, mh, mwMode, mhMode, ownerWidth, ownerHeight, false)
2163
+ const b = isMainRow ? child.layout.width : child.layout.height
2164
+ child._fbBasis = b
2165
+ child._fbOwnerW = ownerWidth
2166
+ child._fbOwnerH = ownerHeight
2167
+ child._fbAvailMain = availableMain
2168
+ child._fbAvailCross = availableCross
2169
+ child._fbCrossMode = crossMode
2170
+ child._fbGen = _generation
2171
+ return b
2172
+ }
2173
+
2174
+ function hasMeasureFuncInSubtree(node: Node): boolean {
2175
+ if (node.measureFunc) return true
2176
+ for (const c of node.children) {
2177
+ if (hasMeasureFuncInSubtree(c)) return true
2178
+ }
2179
+ return false
2180
+ }
2181
+
2182
+ function resolveFlexibleLengths(
2183
+ children: Node[],
2184
+ availableInnerMain: number,
2185
+ totalFlexBasis: number,
2186
+ isMainRow: boolean,
2187
+ ownerW: number,
2188
+ ownerH: number,
2189
+ ): void {
2190
+ // Multi-pass flex distribution per CSS flexbox spec §9.7 "Resolving Flexible
2191
+ // Lengths": distribute free space, detect min/max violations, freeze all
2192
+ // violators, redistribute among unfrozen children. Repeat until stable.
2193
+ const n = children.length
2194
+ const frozen: boolean[] = new Array(n).fill(false)
2195
+ const initialFree = isDefined(availableInnerMain)
2196
+ ? availableInnerMain - totalFlexBasis
2197
+ : 0
2198
+ // Freeze inflexible items at their clamped basis
2199
+ for (let i = 0; i < n; i++) {
2200
+ const c = children[i]!
2201
+ const clamped = boundAxis(c.style, isMainRow, c._flexBasis, ownerW, ownerH)
2202
+ const inflexible =
2203
+ !isDefined(availableInnerMain) ||
2204
+ (initialFree >= 0 ? c.style.flexGrow === 0 : c.style.flexShrink === 0)
2205
+ if (inflexible) {
2206
+ c._mainSize = Math.max(0, clamped)
2207
+ frozen[i] = true
2208
+ } else {
2209
+ c._mainSize = c._flexBasis
2210
+ }
2211
+ }
2212
+ // Iteratively distribute until no violations. Free space is recomputed each
2213
+ // pass: initial free space minus the delta frozen children consumed beyond
2214
+ // (or below) their basis.
2215
+ const unclamped: number[] = new Array(n)
2216
+ for (let iter = 0; iter <= n; iter++) {
2217
+ let frozenDelta = 0
2218
+ let totalGrow = 0
2219
+ let totalShrinkScaled = 0
2220
+ let unfrozenCount = 0
2221
+ for (let i = 0; i < n; i++) {
2222
+ const c = children[i]!
2223
+ if (frozen[i]) {
2224
+ frozenDelta += c._mainSize - c._flexBasis
2225
+ } else {
2226
+ totalGrow += c.style.flexGrow
2227
+ totalShrinkScaled += c.style.flexShrink * c._flexBasis
2228
+ unfrozenCount++
2229
+ }
2230
+ }
2231
+ if (unfrozenCount === 0) break
2232
+ let remaining = initialFree - frozenDelta
2233
+ // Spec §9.7 step 4c: if sum of flex factors < 1, only distribute
2234
+ // initialFree × sum, not the full remaining space (partial flex).
2235
+ if (remaining > 0 && totalGrow > 0 && totalGrow < 1) {
2236
+ const scaled = initialFree * totalGrow
2237
+ if (scaled < remaining) remaining = scaled
2238
+ } else if (remaining < 0 && totalShrinkScaled > 0) {
2239
+ let totalShrink = 0
2240
+ for (let i = 0; i < n; i++) {
2241
+ if (!frozen[i]) totalShrink += children[i]!.style.flexShrink
2242
+ }
2243
+ if (totalShrink < 1) {
2244
+ const scaled = initialFree * totalShrink
2245
+ if (scaled > remaining) remaining = scaled
2246
+ }
2247
+ }
2248
+ // Compute targets + violations for all unfrozen children
2249
+ let totalViolation = 0
2250
+ for (let i = 0; i < n; i++) {
2251
+ if (frozen[i]) continue
2252
+ const c = children[i]!
2253
+ let t = c._flexBasis
2254
+ if (remaining > 0 && totalGrow > 0) {
2255
+ t += (remaining * c.style.flexGrow) / totalGrow
2256
+ } else if (remaining < 0 && totalShrinkScaled > 0) {
2257
+ t +=
2258
+ (remaining * (c.style.flexShrink * c._flexBasis)) / totalShrinkScaled
2259
+ }
2260
+ unclamped[i] = t
2261
+ const clamped = Math.max(
2262
+ 0,
2263
+ boundAxis(c.style, isMainRow, t, ownerW, ownerH),
2264
+ )
2265
+ c._mainSize = clamped
2266
+ totalViolation += clamped - t
2267
+ }
2268
+ // Freeze per spec §9.7 step 5: if totalViolation is zero freeze all; if
2269
+ // positive freeze min-violators; if negative freeze max-violators.
2270
+ if (totalViolation === 0) break
2271
+ let anyFrozen = false
2272
+ for (let i = 0; i < n; i++) {
2273
+ if (frozen[i]) continue
2274
+ const v = children[i]!._mainSize - unclamped[i]!
2275
+ if ((totalViolation > 0 && v > 0) || (totalViolation < 0 && v < 0)) {
2276
+ frozen[i] = true
2277
+ anyFrozen = true
2278
+ }
2279
+ }
2280
+ if (!anyFrozen) break
2281
+ }
2282
+ }
2283
+
2284
+ function isStretchAlign(child: Node): boolean {
2285
+ const p = child.parent
2286
+ if (!p) return false
2287
+ const align =
2288
+ child.style.alignSelf === Align.Auto
2289
+ ? p.style.alignItems
2290
+ : child.style.alignSelf
2291
+ return align === Align.Stretch
2292
+ }
2293
+
2294
+ function resolveChildAlign(parent: Node, child: Node): Align {
2295
+ return child.style.alignSelf === Align.Auto
2296
+ ? parent.style.alignItems
2297
+ : child.style.alignSelf
2298
+ }
2299
+
2300
+ // Baseline of a node per CSS Flexbox §8.5 / yoga's YGBaseline. Leaf nodes
2301
+ // (no children) use their own height. Containers recurse into the first
2302
+ // baseline-aligned child on the first line (or the first flow child if none
2303
+ // are baseline-aligned), returning that child's baseline + its top offset.
2304
+ function calculateBaseline(node: Node): number {
2305
+ let baselineChild: Node | null = null
2306
+ for (const c of node.children) {
2307
+ if (c._lineIndex > 0) break
2308
+ if (c.style.positionType === PositionType.Absolute) continue
2309
+ if (c.style.display === Display.None) continue
2310
+ if (
2311
+ resolveChildAlign(node, c) === Align.Baseline ||
2312
+ c.isReferenceBaseline_
2313
+ ) {
2314
+ baselineChild = c
2315
+ break
2316
+ }
2317
+ if (baselineChild === null) baselineChild = c
2318
+ }
2319
+ if (baselineChild === null) return node.layout.height
2320
+ return calculateBaseline(baselineChild) + baselineChild.layout.top
2321
+ }
2322
+
2323
+ // A container uses baseline layout only for row direction, when either
2324
+ // align-items is baseline or any flow child has align-self: baseline.
2325
+ function isBaselineLayout(node: Node, flowChildren: Node[]): boolean {
2326
+ if (!isRow(node.style.flexDirection)) return false
2327
+ if (node.style.alignItems === Align.Baseline) return true
2328
+ for (const c of flowChildren) {
2329
+ if (c.style.alignSelf === Align.Baseline) return true
2330
+ }
2331
+ return false
2332
+ }
2333
+
2334
+ function childMarginForAxis(
2335
+ child: Node,
2336
+ axis: FlexDirection,
2337
+ ownerWidth: number,
2338
+ ): number {
2339
+ if (!child._hasMargin) return 0
2340
+ const lead = resolveEdge(child.style.margin, leadingEdge(axis), ownerWidth)
2341
+ const trail = resolveEdge(child.style.margin, trailingEdge(axis), ownerWidth)
2342
+ return lead + trail
2343
+ }
2344
+
2345
+ function resolveGap(style: Style, gutter: Gutter, ownerSize: number): number {
2346
+ let v = style.gap[gutter]!
2347
+ if (v.unit === Unit.Undefined) v = style.gap[Gutter.All]!
2348
+ const r = resolveValue(v, ownerSize)
2349
+ return isDefined(r) ? Math.max(0, r) : 0
2350
+ }
2351
+
2352
+ function boundAxis(
2353
+ style: Style,
2354
+ isWidth: boolean,
2355
+ value: number,
2356
+ ownerWidth: number,
2357
+ ownerHeight: number,
2358
+ ): number {
2359
+ const minV = isWidth ? style.minWidth : style.minHeight
2360
+ const maxV = isWidth ? style.maxWidth : style.maxHeight
2361
+ const minU = minV.unit
2362
+ const maxU = maxV.unit
2363
+ // Fast path: no min/max constraints set. Per CPU profile this is the
2364
+ // overwhelmingly common case (~32k calls/layout on the 1000-node bench,
2365
+ // nearly all with undefined min/max) — skipping 2× resolveValue + 2× isNaN
2366
+ // that always no-op. Unit.Undefined = 0.
2367
+ if (minU === 0 && maxU === 0) return value
2368
+ const owner = isWidth ? ownerWidth : ownerHeight
2369
+ let v = value
2370
+ // Inlined resolveValue: Unit.Point=1, Unit.Percent=2. `m === m` is !isNaN.
2371
+ if (maxU === 1) {
2372
+ if (v > maxV.value) v = maxV.value
2373
+ } else if (maxU === 2) {
2374
+ const m = (maxV.value * owner) / 100
2375
+ if (m === m && v > m) v = m
2376
+ }
2377
+ if (minU === 1) {
2378
+ if (v < minV.value) v = minV.value
2379
+ } else if (minU === 2) {
2380
+ const m = (minV.value * owner) / 100
2381
+ if (m === m && v < m) v = m
2382
+ }
2383
+ return v
2384
+ }
2385
+
2386
+ function zeroLayoutRecursive(node: Node): void {
2387
+ for (const c of node.children) {
2388
+ c.layout.left = 0
2389
+ c.layout.top = 0
2390
+ c.layout.width = 0
2391
+ c.layout.height = 0
2392
+ // Invalidate layout cache — without this, unhide → calculateLayout finds
2393
+ // the child clean (!isDirty_) with _hasL intact, hits the cache at line
2394
+ // ~1086, restores stale _lOutW/_lOutH, and returns early — skipping the
2395
+ // child-positioning recursion. Grandchildren stay at (0,0,0,0) from the
2396
+ // zeroing above and render invisible. isDirty_=true also gates _cN and
2397
+ // _fbBasis via their (sameGen || !isDirty_) checks — _cGen/_fbGen freeze
2398
+ // during hide so sameGen is false on unhide.
2399
+ c.isDirty_ = true
2400
+ c._hasL = false
2401
+ c._hasM = false
2402
+ zeroLayoutRecursive(c)
2403
+ }
2404
+ }
2405
+
2406
+ function collectLayoutChildren(node: Node, flow: Node[], abs: Node[]): void {
2407
+ // Partition a node's children into flow and absolute lists, flattening
2408
+ // display:contents subtrees so their children are laid out as direct
2409
+ // children of this node (per CSS display:contents spec — the box is removed
2410
+ // from the layout tree but its children remain, lifted to the grandparent).
2411
+ for (const c of node.children) {
2412
+ const disp = c.style.display
2413
+ if (disp === Display.None) {
2414
+ c.layout.left = 0
2415
+ c.layout.top = 0
2416
+ c.layout.width = 0
2417
+ c.layout.height = 0
2418
+ zeroLayoutRecursive(c)
2419
+ } else if (disp === Display.Contents) {
2420
+ c.layout.left = 0
2421
+ c.layout.top = 0
2422
+ c.layout.width = 0
2423
+ c.layout.height = 0
2424
+ // Recurse — nested display:contents lifts all the way up. The contents
2425
+ // node's own margin/padding/position/dimensions are ignored.
2426
+ collectLayoutChildren(c, flow, abs)
2427
+ } else if (c.style.positionType === PositionType.Absolute) {
2428
+ abs.push(c)
2429
+ } else {
2430
+ flow.push(c)
2431
+ }
2432
+ }
2433
+ }
2434
+
2435
+ function roundLayout(
2436
+ node: Node,
2437
+ scale: number,
2438
+ absLeft: number,
2439
+ absTop: number,
2440
+ ): void {
2441
+ if (scale === 0) return
2442
+ const l = node.layout
2443
+ const nodeLeft = l.left
2444
+ const nodeTop = l.top
2445
+ const nodeWidth = l.width
2446
+ const nodeHeight = l.height
2447
+
2448
+ const absNodeLeft = absLeft + nodeLeft
2449
+ const absNodeTop = absTop + nodeTop
2450
+
2451
+ // Upstream YGRoundValueToPixelGrid: text nodes (has measureFunc) floor their
2452
+ // positions so wrapped text never starts past its allocated column. Width
2453
+ // uses ceil-if-fractional to avoid clipping the last glyph. Non-text nodes
2454
+ // use standard round. Matches yoga's PixelGrid.cpp — without this, justify
2455
+ // center/space-evenly positions are off-by-one vs WASM and flex-shrink
2456
+ // overflow places siblings at the wrong column.
2457
+ const isText = node.measureFunc !== null
2458
+ l.left = roundValue(nodeLeft, scale, false, isText)
2459
+ l.top = roundValue(nodeTop, scale, false, isText)
2460
+
2461
+ // Width/height rounded via absolute edges to avoid cumulative drift
2462
+ const absRight = absNodeLeft + nodeWidth
2463
+ const absBottom = absNodeTop + nodeHeight
2464
+ const hasFracW = !isWholeNumber(nodeWidth * scale)
2465
+ const hasFracH = !isWholeNumber(nodeHeight * scale)
2466
+ l.width =
2467
+ roundValue(absRight, scale, isText && hasFracW, isText && !hasFracW) -
2468
+ roundValue(absNodeLeft, scale, false, isText)
2469
+ l.height =
2470
+ roundValue(absBottom, scale, isText && hasFracH, isText && !hasFracH) -
2471
+ roundValue(absNodeTop, scale, false, isText)
2472
+
2473
+ for (const c of node.children) {
2474
+ roundLayout(c, scale, absNodeLeft, absNodeTop)
2475
+ }
2476
+ }
2477
+
2478
+ function isWholeNumber(v: number): boolean {
2479
+ const frac = v - Math.floor(v)
2480
+ return frac < 0.0001 || frac > 0.9999
2481
+ }
2482
+
2483
+ function roundValue(
2484
+ v: number,
2485
+ scale: number,
2486
+ forceCeil: boolean,
2487
+ forceFloor: boolean,
2488
+ ): number {
2489
+ let scaled = v * scale
2490
+ let frac = scaled - Math.floor(scaled)
2491
+ if (frac < 0) frac += 1
2492
+ // Float-epsilon tolerance matches upstream YGDoubleEqual (1e-4)
2493
+ if (frac < 0.0001) {
2494
+ scaled = Math.floor(scaled)
2495
+ } else if (frac > 0.9999) {
2496
+ scaled = Math.ceil(scaled)
2497
+ } else if (forceCeil) {
2498
+ scaled = Math.ceil(scaled)
2499
+ } else if (forceFloor) {
2500
+ scaled = Math.floor(scaled)
2501
+ } else {
2502
+ // Round half-up (>= 0.5 goes up), per upstream
2503
+ scaled = Math.floor(scaled) + (frac >= 0.4999 ? 1 : 0)
2504
+ }
2505
+ return scaled / scale
2506
+ }
2507
+
2508
+ // --
2509
+ // Helpers
2510
+
2511
+ function parseDimension(v: number | string | undefined): Value {
2512
+ if (v === undefined) return UNDEFINED_VALUE
2513
+ if (v === 'auto') return AUTO_VALUE
2514
+ if (typeof v === 'number') {
2515
+ // WASM yoga's YGFloatIsUndefined treats NaN and ±Infinity as undefined.
2516
+ // Ink passes height={Infinity} (e.g. LogSelector maxHeight default) and
2517
+ // expects it to mean "unconstrained" — storing it as a literal point value
2518
+ // makes the node height Infinity and breaks all downstream layout.
2519
+ return Number.isFinite(v) ? pointValue(v) : UNDEFINED_VALUE
2520
+ }
2521
+ if (typeof v === 'string' && v.endsWith('%')) {
2522
+ return percentValue(parseFloat(v))
2523
+ }
2524
+ const n = parseFloat(v)
2525
+ return isNaN(n) ? UNDEFINED_VALUE : pointValue(n)
2526
+ }
2527
+
2528
+ function physicalEdge(edge: Edge): number {
2529
+ switch (edge) {
2530
+ case Edge.Left:
2531
+ case Edge.Start:
2532
+ return EDGE_LEFT
2533
+ case Edge.Top:
2534
+ return EDGE_TOP
2535
+ case Edge.Right:
2536
+ case Edge.End:
2537
+ return EDGE_RIGHT
2538
+ case Edge.Bottom:
2539
+ return EDGE_BOTTOM
2540
+ default:
2541
+ return EDGE_LEFT
2542
+ }
2543
+ }
2544
+
2545
+ // --
2546
+ // Module API matching yoga-layout/load
2547
+
2548
+ export type Yoga = {
2549
+ Config: {
2550
+ create(): Config
2551
+ destroy(config: Config): void
2552
+ }
2553
+ Node: {
2554
+ create(config?: Config): Node
2555
+ createDefault(): Node
2556
+ createWithConfig(config: Config): Node
2557
+ destroy(node: Node): void
2558
+ }
2559
+ }
2560
+
2561
+ const YOGA_INSTANCE: Yoga = {
2562
+ Config: {
2563
+ create: createConfig,
2564
+ destroy() {},
2565
+ },
2566
+ Node: {
2567
+ create: (config?: Config) => new Node(config),
2568
+ createDefault: () => new Node(),
2569
+ createWithConfig: (config: Config) => new Node(config),
2570
+ destroy() {},
2571
+ },
2572
+ }
2573
+
2574
+ export function loadYoga(): Promise<Yoga> {
2575
+ return Promise.resolve(YOGA_INSTANCE)
2576
+ }
2577
+
2578
+ export default YOGA_INSTANCE