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.
- package/assistant/sessionHistory.ts +87 -0
- package/bootstrap/state.ts +1769 -0
- package/bridge/bridgeApi.ts +539 -0
- package/bridge/bridgeConfig.ts +48 -0
- package/bridge/bridgeDebug.ts +135 -0
- package/bridge/bridgeEnabled.ts +202 -0
- package/bridge/bridgeMain.ts +2999 -0
- package/bridge/bridgeMessaging.ts +461 -0
- package/bridge/bridgePermissionCallbacks.ts +43 -0
- package/bridge/bridgePointer.ts +210 -0
- package/bridge/bridgeStatusUtil.ts +163 -0
- package/bridge/bridgeUI.ts +530 -0
- package/bridge/capacityWake.ts +56 -0
- package/bridge/codeSessionApi.ts +168 -0
- package/bridge/createSession.ts +384 -0
- package/bridge/debugUtils.ts +141 -0
- package/bridge/envLessBridgeConfig.ts +165 -0
- package/bridge/flushGate.ts +71 -0
- package/bridge/inboundAttachments.ts +175 -0
- package/bridge/inboundMessages.ts +80 -0
- package/bridge/initReplBridge.ts +569 -0
- package/bridge/jwtUtils.ts +256 -0
- package/bridge/pollConfig.ts +110 -0
- package/bridge/pollConfigDefaults.ts +82 -0
- package/bridge/remoteBridgeCore.ts +1008 -0
- package/bridge/replBridge.ts +2406 -0
- package/bridge/replBridgeHandle.ts +36 -0
- package/bridge/replBridgeTransport.ts +370 -0
- package/bridge/sessionIdCompat.ts +57 -0
- package/bridge/sessionRunner.ts +550 -0
- package/bridge/trustedDevice.ts +210 -0
- package/bridge/types.ts +262 -0
- package/bridge/workSecret.ts +127 -0
- package/buddy/CompanionSprite.tsx +371 -0
- package/buddy/companion.ts +133 -0
- package/buddy/prompt.ts +36 -0
- package/buddy/sprites.ts +514 -0
- package/buddy/types.ts +148 -0
- package/buddy/useBuddyNotification.tsx +98 -0
- package/coordinator/coordinatorMode.ts +369 -0
- package/memdir/findRelevantMemories.ts +141 -0
- package/memdir/memdir.ts +507 -0
- package/memdir/memoryAge.ts +53 -0
- package/memdir/memoryScan.ts +94 -0
- package/memdir/memoryTypes.ts +271 -0
- package/memdir/paths.ts +278 -0
- package/memdir/teamMemPaths.ts +292 -0
- package/memdir/teamMemPrompts.ts +100 -0
- package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
- package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
- package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
- package/migrations/migrateFennecToOpus.ts +45 -0
- package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
- package/migrations/migrateOpusToOpus1m.ts +43 -0
- package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
- package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
- package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
- package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
- package/migrations/resetProToOpusDefault.ts +51 -0
- package/native-ts/color-diff/index.ts +999 -0
- package/native-ts/file-index/index.ts +370 -0
- package/native-ts/yoga-layout/enums.ts +134 -0
- package/native-ts/yoga-layout/index.ts +2578 -0
- package/outputStyles/loadOutputStylesDir.ts +98 -0
- package/package.json +22 -5
- package/plugins/builtinPlugins.ts +159 -0
- package/plugins/bundled/index.ts +23 -0
- package/schemas/hooks.ts +222 -0
- package/screens/Doctor.tsx +575 -0
- package/screens/REPL.tsx +5006 -0
- package/screens/ResumeConversation.tsx +399 -0
- package/server/createDirectConnectSession.ts +88 -0
- package/server/directConnectManager.ts +213 -0
- package/server/types.ts +57 -0
- package/skills/bundled/batch.ts +124 -0
- package/skills/bundled/claudeApi.ts +196 -0
- package/skills/bundled/claudeApiContent.ts +75 -0
- package/skills/bundled/claudeInChrome.ts +34 -0
- package/skills/bundled/debug.ts +103 -0
- package/skills/bundled/index.ts +79 -0
- package/skills/bundled/keybindings.ts +339 -0
- package/skills/bundled/loop.ts +92 -0
- package/skills/bundled/loremIpsum.ts +282 -0
- package/skills/bundled/remember.ts +82 -0
- package/skills/bundled/scheduleRemoteAgents.ts +447 -0
- package/skills/bundled/simplify.ts +69 -0
- package/skills/bundled/skillify.ts +197 -0
- package/skills/bundled/stuck.ts +79 -0
- package/skills/bundled/updateConfig.ts +475 -0
- package/skills/bundled/verify/SKILL.md +3 -0
- package/skills/bundled/verify/examples/cli.md +3 -0
- package/skills/bundled/verify/examples/server.md +3 -0
- package/skills/bundled/verify.ts +30 -0
- package/skills/bundled/verifyContent.ts +13 -0
- package/skills/bundledSkills.ts +220 -0
- package/skills/loadSkillsDir.ts +1086 -0
- package/skills/mcpSkillBuilders.ts +44 -0
- package/tasks/DreamTask/DreamTask.ts +157 -0
- package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
- package/tasks/InProcessTeammateTask/types.ts +121 -0
- package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
- package/tasks/LocalMainSessionTask.ts +479 -0
- package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
- package/tasks/LocalShellTask/guards.ts +41 -0
- package/tasks/LocalShellTask/killShellTasks.ts +76 -0
- package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
- package/tasks/pillLabel.ts +82 -0
- package/tasks/stopTask.ts +100 -0
- package/tasks/types.ts +46 -0
- package/upstreamproxy/relay.ts +455 -0
- package/upstreamproxy/upstreamproxy.ts +285 -0
- package/vim/motions.ts +82 -0
- package/vim/operators.ts +556 -0
- package/vim/textObjects.ts +186 -0
- package/vim/transitions.ts +490 -0
- package/vim/types.ts +199 -0
- 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
|