@rlabs-inc/tui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -0
- package/index.ts +45 -0
- package/package.json +59 -0
- package/src/api/index.ts +7 -0
- package/src/api/mount.ts +230 -0
- package/src/engine/arrays/core.ts +60 -0
- package/src/engine/arrays/dimensions.ts +68 -0
- package/src/engine/arrays/index.ts +166 -0
- package/src/engine/arrays/interaction.ts +112 -0
- package/src/engine/arrays/layout.ts +175 -0
- package/src/engine/arrays/spacing.ts +100 -0
- package/src/engine/arrays/text.ts +55 -0
- package/src/engine/arrays/visual.ts +140 -0
- package/src/engine/index.ts +25 -0
- package/src/engine/inheritance.ts +138 -0
- package/src/engine/registry.ts +180 -0
- package/src/pipeline/frameBuffer.ts +473 -0
- package/src/pipeline/layout/index.ts +105 -0
- package/src/pipeline/layout/titan-engine.ts +798 -0
- package/src/pipeline/layout/types.ts +194 -0
- package/src/pipeline/layout/utils/hierarchy.ts +202 -0
- package/src/pipeline/layout/utils/math.ts +134 -0
- package/src/pipeline/layout/utils/text-measure.ts +160 -0
- package/src/pipeline/layout.ts +30 -0
- package/src/primitives/box.ts +312 -0
- package/src/primitives/index.ts +12 -0
- package/src/primitives/text.ts +199 -0
- package/src/primitives/types.ts +222 -0
- package/src/primitives/utils.ts +37 -0
- package/src/renderer/ansi.ts +625 -0
- package/src/renderer/buffer.ts +667 -0
- package/src/renderer/index.ts +40 -0
- package/src/renderer/input.ts +518 -0
- package/src/renderer/output.ts +451 -0
- package/src/state/cursor.ts +176 -0
- package/src/state/focus.ts +241 -0
- package/src/state/index.ts +43 -0
- package/src/state/keyboard.ts +771 -0
- package/src/state/mouse.ts +524 -0
- package/src/state/scroll.ts +341 -0
- package/src/state/theme.ts +687 -0
- package/src/types/color.ts +401 -0
- package/src/types/index.ts +316 -0
- package/src/utils/text.ts +471 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - TITAN Layout Engine v3
|
|
3
|
+
*
|
|
4
|
+
* Complete terminal layout system using parallel arrays.
|
|
5
|
+
* Module-level arrays for speed, reset function for cleanup.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Block layout (vertical stacking)
|
|
9
|
+
* - Flexbox (grow/shrink/wrap/justify/align)
|
|
10
|
+
* - Absolute/Fixed positioning
|
|
11
|
+
*
|
|
12
|
+
* Memory model:
|
|
13
|
+
* - Arrays are module-level (reused for speed)
|
|
14
|
+
* - Call resetTitanArrays() after destroying all components
|
|
15
|
+
* (in benchmarks, tests, or major UI transitions)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { unwrap } from '@rlabs-inc/signals'
|
|
19
|
+
import { ComponentType } from '../../types'
|
|
20
|
+
import * as core from '../../engine/arrays/core'
|
|
21
|
+
import * as dimensions from '../../engine/arrays/dimensions'
|
|
22
|
+
import * as spacing from '../../engine/arrays/spacing'
|
|
23
|
+
import * as layout from '../../engine/arrays/layout'
|
|
24
|
+
import * as visual from '../../engine/arrays/visual'
|
|
25
|
+
import * as text from '../../engine/arrays/text'
|
|
26
|
+
import { stringWidth } from '../../utils/text'
|
|
27
|
+
import { measureTextHeight } from './utils/text-measure'
|
|
28
|
+
import { getAllocatedIndices } from '../../engine/registry'
|
|
29
|
+
|
|
30
|
+
import type { ComputedLayout } from './types'
|
|
31
|
+
import { Overflow } from './types'
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// ENUMS (match layout.ts)
|
|
35
|
+
// =============================================================================
|
|
36
|
+
const FLEX_ROW = 1
|
|
37
|
+
const FLEX_ROW_REVERSE = 3
|
|
38
|
+
const FLEX_COLUMN = 0
|
|
39
|
+
const FLEX_COLUMN_REVERSE = 2
|
|
40
|
+
|
|
41
|
+
const WRAP_NOWRAP = 0
|
|
42
|
+
const WRAP_WRAP = 1
|
|
43
|
+
const WRAP_REVERSE = 2
|
|
44
|
+
|
|
45
|
+
const JUSTIFY_START = 0
|
|
46
|
+
const JUSTIFY_CENTER = 1
|
|
47
|
+
const JUSTIFY_END = 2
|
|
48
|
+
const JUSTIFY_BETWEEN = 3
|
|
49
|
+
const JUSTIFY_AROUND = 4
|
|
50
|
+
const JUSTIFY_EVENLY = 5
|
|
51
|
+
|
|
52
|
+
const ALIGN_STRETCH = 0
|
|
53
|
+
const ALIGN_START = 1
|
|
54
|
+
const ALIGN_CENTER = 2
|
|
55
|
+
const ALIGN_END = 3
|
|
56
|
+
|
|
57
|
+
const POS_RELATIVE = 0
|
|
58
|
+
const POS_ABSOLUTE = 1
|
|
59
|
+
|
|
60
|
+
// Align-self: 0 = auto (use parent's alignItems), 1+ = same as ALIGN_* constants
|
|
61
|
+
const ALIGN_SELF_AUTO = 0
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// DIMENSION RESOLVER - Handles both absolute and percentage values
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a dimension value against a parent size.
|
|
69
|
+
* - number: Return as-is (absolute value)
|
|
70
|
+
* - string like '50%': Return parentSize * percentage / 100
|
|
71
|
+
*
|
|
72
|
+
* Performance: Inline check, no function call overhead for common case.
|
|
73
|
+
*/
|
|
74
|
+
function resolveDim(dim: number | string | null | undefined, parentSize: number): number {
|
|
75
|
+
if (dim == null) return 0
|
|
76
|
+
if (typeof dim === 'number') return dim
|
|
77
|
+
// String percentage like '50%' - parse and resolve
|
|
78
|
+
return Math.floor(parentSize * parseFloat(dim) / 100)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Apply min/max constraints to a dimension.
|
|
83
|
+
* Resolves percentage constraints against parentSize.
|
|
84
|
+
*/
|
|
85
|
+
function clampDim(
|
|
86
|
+
value: number,
|
|
87
|
+
minVal: number | string | null | undefined,
|
|
88
|
+
maxVal: number | string | null | undefined,
|
|
89
|
+
parentSize: number
|
|
90
|
+
): number {
|
|
91
|
+
const min = resolveDim(minVal, parentSize)
|
|
92
|
+
const max = resolveDim(maxVal, parentSize)
|
|
93
|
+
|
|
94
|
+
let result = value
|
|
95
|
+
if (min > 0 && result < min) result = min
|
|
96
|
+
if (max > 0 && result > max) result = max
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// MODULE-LEVEL ARRAYS (reused for speed)
|
|
102
|
+
// =============================================================================
|
|
103
|
+
const outX: number[] = []
|
|
104
|
+
const outY: number[] = []
|
|
105
|
+
const outW: number[] = []
|
|
106
|
+
const outH: number[] = []
|
|
107
|
+
const outScrollable: number[] = []
|
|
108
|
+
const outMaxScrollX: number[] = []
|
|
109
|
+
const outMaxScrollY: number[] = []
|
|
110
|
+
|
|
111
|
+
const firstChild: number[] = []
|
|
112
|
+
const nextSibling: number[] = []
|
|
113
|
+
const lastChild: number[] = []
|
|
114
|
+
|
|
115
|
+
const intrinsicW: number[] = []
|
|
116
|
+
const intrinsicH: number[] = []
|
|
117
|
+
|
|
118
|
+
const itemMain: number[] = []
|
|
119
|
+
const itemCross: number[] = []
|
|
120
|
+
|
|
121
|
+
// Working arrays for layoutChildren (reused to avoid per-call allocation)
|
|
122
|
+
const flowKids: number[] = []
|
|
123
|
+
const lineStarts: number[] = []
|
|
124
|
+
const lineEnds: number[] = []
|
|
125
|
+
const lineMainUsed: number[] = []
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// INTRINSIC CACHE - Skip recomputation when inputs unchanged
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// For TEXT components: cache based on text content hash + available width
|
|
131
|
+
// For BOX components: cache based on children intrinsics + layout props
|
|
132
|
+
const cachedTextHash: bigint[] = []
|
|
133
|
+
const cachedAvailW: number[] = []
|
|
134
|
+
const cachedIntrinsicW: number[] = []
|
|
135
|
+
const cachedIntrinsicH: number[] = []
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// RESET FUNCTION - Call after destroying all components
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reset all TITAN working arrays to release memory.
|
|
143
|
+
* Call this after destroying all components:
|
|
144
|
+
* - In benchmarks between test runs
|
|
145
|
+
* - In tests during cleanup
|
|
146
|
+
* - In runtime after major UI transitions (e.g., switching screens)
|
|
147
|
+
*/
|
|
148
|
+
export function resetTitanArrays(): void {
|
|
149
|
+
outX.length = 0
|
|
150
|
+
outY.length = 0
|
|
151
|
+
outW.length = 0
|
|
152
|
+
outH.length = 0
|
|
153
|
+
outScrollable.length = 0
|
|
154
|
+
outMaxScrollX.length = 0
|
|
155
|
+
outMaxScrollY.length = 0
|
|
156
|
+
firstChild.length = 0
|
|
157
|
+
nextSibling.length = 0
|
|
158
|
+
lastChild.length = 0
|
|
159
|
+
intrinsicW.length = 0
|
|
160
|
+
intrinsicH.length = 0
|
|
161
|
+
itemMain.length = 0
|
|
162
|
+
itemCross.length = 0
|
|
163
|
+
flowKids.length = 0
|
|
164
|
+
lineStarts.length = 0
|
|
165
|
+
lineEnds.length = 0
|
|
166
|
+
lineMainUsed.length = 0
|
|
167
|
+
// Intrinsic cache
|
|
168
|
+
cachedTextHash.length = 0
|
|
169
|
+
cachedAvailW.length = 0
|
|
170
|
+
cachedIntrinsicW.length = 0
|
|
171
|
+
cachedIntrinsicH.length = 0
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// MAIN ENTRY
|
|
176
|
+
// =============================================================================
|
|
177
|
+
|
|
178
|
+
export function computeLayoutTitan(
|
|
179
|
+
terminalWidth: number,
|
|
180
|
+
terminalHeight: number,
|
|
181
|
+
providedIndices?: Set<number>,
|
|
182
|
+
constrainHeight: boolean = true // false for inline/append modes
|
|
183
|
+
): ComputedLayout {
|
|
184
|
+
const indices = providedIndices ?? getAllocatedIndices()
|
|
185
|
+
|
|
186
|
+
if (indices.size === 0) {
|
|
187
|
+
return { x: [], y: [], width: [], height: [], scrollable: [], maxScrollY: [], maxScrollX: [], contentWidth: 0, contentHeight: 0 }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
191
|
+
// PASS 1: Build linked-list tree structure (O(n))
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
193
|
+
for (const i of indices) {
|
|
194
|
+
firstChild[i] = -1
|
|
195
|
+
nextSibling[i] = -1
|
|
196
|
+
lastChild[i] = -1
|
|
197
|
+
intrinsicW[i] = 0
|
|
198
|
+
intrinsicH[i] = 0
|
|
199
|
+
outScrollable[i] = 0
|
|
200
|
+
outMaxScrollX[i] = 0
|
|
201
|
+
outMaxScrollY[i] = 0
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const bfsQueue: number[] = []
|
|
205
|
+
let rootCount = 0
|
|
206
|
+
|
|
207
|
+
for (const i of indices) {
|
|
208
|
+
// Skip invisible components - they don't participate in layout
|
|
209
|
+
const vis = unwrap(core.visible[i])
|
|
210
|
+
if (vis === 0 || vis === false) continue
|
|
211
|
+
|
|
212
|
+
const parent = unwrap(core.parentIndex[i]) ?? -1
|
|
213
|
+
|
|
214
|
+
if (parent >= 0 && indices.has(parent)) {
|
|
215
|
+
if (firstChild[parent] === -1) {
|
|
216
|
+
firstChild[parent] = i
|
|
217
|
+
} else {
|
|
218
|
+
nextSibling[lastChild[parent]!] = i
|
|
219
|
+
}
|
|
220
|
+
lastChild[parent] = i
|
|
221
|
+
} else {
|
|
222
|
+
bfsQueue[rootCount++] = i
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
227
|
+
// PASS 2: BFS traversal - parents before children order
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
229
|
+
let head = 0
|
|
230
|
+
while (head < bfsQueue.length) {
|
|
231
|
+
const parent = bfsQueue[head++]!
|
|
232
|
+
let child = firstChild[parent]!
|
|
233
|
+
while (child !== -1) {
|
|
234
|
+
bfsQueue.push(child)
|
|
235
|
+
child = nextSibling[child]!
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
240
|
+
// PASS 3: Measure intrinsic sizes (bottom-up)
|
|
241
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
242
|
+
for (let si = bfsQueue.length - 1; si >= 0; si--) {
|
|
243
|
+
const i = bfsQueue[si]!
|
|
244
|
+
const type = core.componentType[i]
|
|
245
|
+
|
|
246
|
+
if (type === ComponentType.TEXT) {
|
|
247
|
+
const content = unwrap(text.textContent[i])
|
|
248
|
+
// Check for null/undefined, NOT truthiness (0 and '' are valid content!)
|
|
249
|
+
if (content != null) {
|
|
250
|
+
const str = String(content)
|
|
251
|
+
|
|
252
|
+
if (str.length > 0) {
|
|
253
|
+
// TEXT WRAPPING: Calculate available width for height measurement
|
|
254
|
+
const parentIdx = unwrap(core.parentIndex[i]) ?? -1
|
|
255
|
+
let availableW = terminalWidth
|
|
256
|
+
|
|
257
|
+
if (parentIdx >= 0) {
|
|
258
|
+
const rawParentW = unwrap(dimensions.width[parentIdx])
|
|
259
|
+
const parentExplicitW = typeof rawParentW === 'number' ? rawParentW : 0
|
|
260
|
+
if (parentExplicitW > 0) {
|
|
261
|
+
const pPadL = unwrap(spacing.paddingLeft[parentIdx]) ?? 0
|
|
262
|
+
const pPadR = unwrap(spacing.paddingRight[parentIdx]) ?? 0
|
|
263
|
+
const pBorderStyle = unwrap(visual.borderStyle[parentIdx]) ?? 0
|
|
264
|
+
const pBorderL = pBorderStyle > 0 || (unwrap(visual.borderLeft[parentIdx]) ?? 0) > 0 ? 1 : 0
|
|
265
|
+
const pBorderR = pBorderStyle > 0 || (unwrap(visual.borderRight[parentIdx]) ?? 0) > 0 ? 1 : 0
|
|
266
|
+
availableW = Math.max(1, parentExplicitW - pPadL - pPadR - pBorderL - pBorderR)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// CACHE CHECK: Hash text content, compare with cached
|
|
271
|
+
// Only recompute stringWidth/measureTextHeight if content or availableW changed
|
|
272
|
+
const textHash = Bun.hash(str)
|
|
273
|
+
if (textHash === cachedTextHash[i] && availableW === cachedAvailW[i]) {
|
|
274
|
+
// Cache hit - reuse cached intrinsics (skip expensive computation!)
|
|
275
|
+
intrinsicW[i] = cachedIntrinsicW[i]!
|
|
276
|
+
intrinsicH[i] = cachedIntrinsicH[i]!
|
|
277
|
+
} else {
|
|
278
|
+
// Cache miss - compute and store
|
|
279
|
+
intrinsicW[i] = stringWidth(str)
|
|
280
|
+
intrinsicH[i] = measureTextHeight(str, availableW)
|
|
281
|
+
cachedTextHash[i] = textHash
|
|
282
|
+
cachedAvailW[i] = availableW
|
|
283
|
+
cachedIntrinsicW[i] = intrinsicW[i]
|
|
284
|
+
cachedIntrinsicH[i] = intrinsicH[i]
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
intrinsicW[i] = 0
|
|
288
|
+
intrinsicH[i] = 0
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// BOX/Container - calculate intrinsic from children + padding + borders
|
|
293
|
+
let kid = firstChild[i]!
|
|
294
|
+
if (kid !== -1) {
|
|
295
|
+
const dir = unwrap(layout.flexDirection[i]) ?? FLEX_COLUMN
|
|
296
|
+
const isRow = dir === FLEX_ROW || dir === FLEX_ROW_REVERSE
|
|
297
|
+
const gap = unwrap(spacing.gap[i]) ?? 0
|
|
298
|
+
|
|
299
|
+
let sumMain = 0
|
|
300
|
+
let maxCross = 0
|
|
301
|
+
let childCount = 0
|
|
302
|
+
|
|
303
|
+
while (kid !== -1) {
|
|
304
|
+
childCount++
|
|
305
|
+
// Use max of explicit dimension and intrinsic size
|
|
306
|
+
// This ensures children with explicit sizes contribute correctly
|
|
307
|
+
// Note: Percentage dimensions (strings) → 0 for intrinsic calculation
|
|
308
|
+
// They'll be resolved against parent computed size in layout phase
|
|
309
|
+
const rawKidW = unwrap(dimensions.width[kid])
|
|
310
|
+
const rawKidH = unwrap(dimensions.height[kid])
|
|
311
|
+
const kidExplicitW = typeof rawKidW === 'number' ? rawKidW : 0
|
|
312
|
+
const kidExplicitH = typeof rawKidH === 'number' ? rawKidH : 0
|
|
313
|
+
const kidW = kidExplicitW > 0 ? kidExplicitW : intrinsicW[kid]!
|
|
314
|
+
const kidH = kidExplicitH > 0 ? kidExplicitH : intrinsicH[kid]!
|
|
315
|
+
|
|
316
|
+
// FIX: Don't add child borders - intrinsicW/H already includes them,
|
|
317
|
+
// and explicit dimensions are total dimensions (including borders).
|
|
318
|
+
// Adding borders here was DOUBLE-COUNTING and inflating contentHeight.
|
|
319
|
+
//
|
|
320
|
+
// OLD CODE (double-counted borders):
|
|
321
|
+
// const kidBs = unwrap(visual.borderStyle[kid]) ?? 0
|
|
322
|
+
// const kidBordT = kidBs > 0 || (unwrap(visual.borderTop[kid]) ?? 0) > 0 ? 1 : 0
|
|
323
|
+
// const kidBordB = kidBs > 0 || (unwrap(visual.borderBottom[kid]) ?? 0) > 0 ? 1 : 0
|
|
324
|
+
// const kidBordL = kidBs > 0 || (unwrap(visual.borderLeft[kid]) ?? 0) > 0 ? 1 : 0
|
|
325
|
+
// const kidBordR = kidBs > 0 || (unwrap(visual.borderRight[kid]) ?? 0) > 0 ? 1 : 0
|
|
326
|
+
// const kidTotalW = kidW + kidBordL + kidBordR
|
|
327
|
+
// const kidTotalH = kidH + kidBordT + kidBordB
|
|
328
|
+
|
|
329
|
+
if (isRow) {
|
|
330
|
+
sumMain += kidW + gap
|
|
331
|
+
maxCross = Math.max(maxCross, kidH)
|
|
332
|
+
} else {
|
|
333
|
+
sumMain += kidH + gap
|
|
334
|
+
maxCross = Math.max(maxCross, kidW)
|
|
335
|
+
}
|
|
336
|
+
kid = nextSibling[kid]!
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (childCount > 0) sumMain -= gap
|
|
340
|
+
|
|
341
|
+
// Add padding and borders to intrinsic size
|
|
342
|
+
const padTop = unwrap(spacing.paddingTop[i]) ?? 0
|
|
343
|
+
const padRight = unwrap(spacing.paddingRight[i]) ?? 0
|
|
344
|
+
const padBottom = unwrap(spacing.paddingBottom[i]) ?? 0
|
|
345
|
+
const padLeft = unwrap(spacing.paddingLeft[i]) ?? 0
|
|
346
|
+
const borderStyle = unwrap(visual.borderStyle[i]) ?? 0
|
|
347
|
+
const borderT = borderStyle > 0 || (unwrap(visual.borderTop[i]) ?? 0) > 0 ? 1 : 0
|
|
348
|
+
const borderR = borderStyle > 0 || (unwrap(visual.borderRight[i]) ?? 0) > 0 ? 1 : 0
|
|
349
|
+
const borderB = borderStyle > 0 || (unwrap(visual.borderBottom[i]) ?? 0) > 0 ? 1 : 0
|
|
350
|
+
const borderL = borderStyle > 0 || (unwrap(visual.borderLeft[i]) ?? 0) > 0 ? 1 : 0
|
|
351
|
+
|
|
352
|
+
const extraWidth = padLeft + padRight + borderL + borderR
|
|
353
|
+
const extraHeight = padTop + padBottom + borderT + borderB
|
|
354
|
+
|
|
355
|
+
if (isRow) {
|
|
356
|
+
intrinsicW[i] = sumMain + extraWidth
|
|
357
|
+
intrinsicH[i] = maxCross + extraHeight
|
|
358
|
+
} else {
|
|
359
|
+
intrinsicW[i] = maxCross + extraWidth
|
|
360
|
+
intrinsicH[i] = sumMain + extraHeight
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
367
|
+
// HELPER: Layout children of a single parent (NON-RECURSIVE)
|
|
368
|
+
// Uses module-level arrays to avoid per-call allocation
|
|
369
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
370
|
+
function layoutChildrenOf(parent: number): void {
|
|
371
|
+
// Reset working arrays (reuse module-level arrays)
|
|
372
|
+
flowKids.length = 0
|
|
373
|
+
lineStarts.length = 0
|
|
374
|
+
lineEnds.length = 0
|
|
375
|
+
lineMainUsed.length = 0
|
|
376
|
+
|
|
377
|
+
// Track children max extent for scroll detection (zero overhead - updated inline)
|
|
378
|
+
let childrenMaxMain = 0
|
|
379
|
+
let childrenMaxCross = 0
|
|
380
|
+
|
|
381
|
+
// Collect flow children
|
|
382
|
+
let kid = firstChild[parent]!
|
|
383
|
+
while (kid !== -1) {
|
|
384
|
+
if ((unwrap(layout.position[kid]) ?? POS_RELATIVE) !== POS_ABSOLUTE) {
|
|
385
|
+
flowKids.push(kid)
|
|
386
|
+
}
|
|
387
|
+
kid = nextSibling[kid]!
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (flowKids.length === 0) return
|
|
391
|
+
|
|
392
|
+
// Parent's content area
|
|
393
|
+
const pPadT = unwrap(spacing.paddingTop[parent]) ?? 0
|
|
394
|
+
const pPadR = unwrap(spacing.paddingRight[parent]) ?? 0
|
|
395
|
+
const pPadB = unwrap(spacing.paddingBottom[parent]) ?? 0
|
|
396
|
+
const pPadL = unwrap(spacing.paddingLeft[parent]) ?? 0
|
|
397
|
+
|
|
398
|
+
const pBs = unwrap(visual.borderStyle[parent]) ?? 0
|
|
399
|
+
const pBordT = pBs > 0 || (unwrap(visual.borderTop[parent]) ?? 0) > 0 ? 1 : 0
|
|
400
|
+
const pBordR = pBs > 0 || (unwrap(visual.borderRight[parent]) ?? 0) > 0 ? 1 : 0
|
|
401
|
+
const pBordB = pBs > 0 || (unwrap(visual.borderBottom[parent]) ?? 0) > 0 ? 1 : 0
|
|
402
|
+
const pBordL = pBs > 0 || (unwrap(visual.borderLeft[parent]) ?? 0) > 0 ? 1 : 0
|
|
403
|
+
|
|
404
|
+
const contentX = outX[parent]! + pPadL + pBordL
|
|
405
|
+
const contentY = outY[parent]! + pPadT + pBordT
|
|
406
|
+
const contentW = Math.max(0, outW[parent]! - pPadL - pPadR - pBordL - pBordR)
|
|
407
|
+
const contentH = Math.max(0, outH[parent]! - pPadT - pPadB - pBordT - pBordB)
|
|
408
|
+
|
|
409
|
+
// Flex properties
|
|
410
|
+
const dir = unwrap(layout.flexDirection[parent]) ?? FLEX_COLUMN
|
|
411
|
+
const wrap = unwrap(layout.flexWrap[parent]) ?? WRAP_NOWRAP
|
|
412
|
+
const justify = unwrap(layout.justifyContent[parent]) ?? JUSTIFY_START
|
|
413
|
+
const alignItems = unwrap(layout.alignItems[parent]) ?? ALIGN_STRETCH
|
|
414
|
+
const gap = unwrap(spacing.gap[parent]) ?? 0
|
|
415
|
+
const overflow = unwrap(layout.overflow[parent]) ?? Overflow.VISIBLE
|
|
416
|
+
|
|
417
|
+
const isRow = dir === FLEX_ROW || dir === FLEX_ROW_REVERSE
|
|
418
|
+
const isReverse = dir === FLEX_ROW_REVERSE || dir === FLEX_COLUMN_REVERSE
|
|
419
|
+
// Scrollable containers should NOT shrink children - content scrolls instead
|
|
420
|
+
const isScrollableParent = overflow === Overflow.SCROLL || overflow === Overflow.AUTO
|
|
421
|
+
|
|
422
|
+
const mainSize = isRow ? contentW : contentH
|
|
423
|
+
const crossSize = isRow ? contentH : contentW
|
|
424
|
+
|
|
425
|
+
// STEP 1: Collect items into flex lines
|
|
426
|
+
// Child dimensions resolve percentages against parent's content area
|
|
427
|
+
let lineStart = 0
|
|
428
|
+
let currentMain = 0
|
|
429
|
+
|
|
430
|
+
for (let fi = 0; fi < flowKids.length; fi++) {
|
|
431
|
+
const fkid = flowKids[fi]!
|
|
432
|
+
const ew = resolveDim(unwrap(dimensions.width[fkid]), contentW)
|
|
433
|
+
const eh = resolveDim(unwrap(dimensions.height[fkid]), contentH)
|
|
434
|
+
const kidMain = isRow
|
|
435
|
+
? (ew > 0 ? ew : intrinsicW[fkid]!)
|
|
436
|
+
: (eh > 0 ? eh : intrinsicH[fkid]!)
|
|
437
|
+
|
|
438
|
+
if (wrap !== WRAP_NOWRAP && fi > lineStart && currentMain + kidMain + gap > mainSize) {
|
|
439
|
+
lineStarts.push(lineStart)
|
|
440
|
+
lineEnds.push(fi - 1)
|
|
441
|
+
lineMainUsed.push(currentMain - gap)
|
|
442
|
+
lineStart = fi
|
|
443
|
+
currentMain = 0
|
|
444
|
+
}
|
|
445
|
+
currentMain += kidMain + gap
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (flowKids.length > 0) {
|
|
449
|
+
lineStarts.push(lineStart)
|
|
450
|
+
lineEnds.push(flowKids.length - 1)
|
|
451
|
+
lineMainUsed.push(currentMain - gap)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const lineCount = lineStarts.length
|
|
455
|
+
|
|
456
|
+
// STEP 2: Resolve flex grow/shrink per line
|
|
457
|
+
for (let li = 0; li < lineCount; li++) {
|
|
458
|
+
const lStart = lineStarts[li]!
|
|
459
|
+
const lEnd = lineEnds[li]!
|
|
460
|
+
const freeSpace = mainSize - lineMainUsed[li]!
|
|
461
|
+
|
|
462
|
+
let totalGrow = 0
|
|
463
|
+
let totalShrink = 0
|
|
464
|
+
|
|
465
|
+
for (let fi = lStart; fi <= lEnd; fi++) {
|
|
466
|
+
const fkid = flowKids[fi]!
|
|
467
|
+
totalGrow += unwrap(layout.flexGrow[fkid]) ?? 0
|
|
468
|
+
totalShrink += unwrap(layout.flexShrink[fkid]) ?? 1
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (let fi = lStart; fi <= lEnd; fi++) {
|
|
472
|
+
const fkid = flowKids[fi]!
|
|
473
|
+
const ew = resolveDim(unwrap(dimensions.width[fkid]), contentW)
|
|
474
|
+
const eh = resolveDim(unwrap(dimensions.height[fkid]), contentH)
|
|
475
|
+
|
|
476
|
+
// flex-basis takes priority over width/height for main axis size
|
|
477
|
+
const basis = unwrap(layout.flexBasis[fkid]) ?? 0
|
|
478
|
+
let kidMain = basis > 0
|
|
479
|
+
? basis
|
|
480
|
+
: (isRow
|
|
481
|
+
? (ew > 0 ? ew : intrinsicW[fkid]!)
|
|
482
|
+
: (eh > 0 ? eh : intrinsicH[fkid]!))
|
|
483
|
+
|
|
484
|
+
if (freeSpace > 0 && totalGrow > 0) {
|
|
485
|
+
kidMain += ((unwrap(layout.flexGrow[fkid]) ?? 0) / totalGrow) * freeSpace
|
|
486
|
+
} else if (freeSpace < 0 && totalShrink > 0 && !isScrollableParent) {
|
|
487
|
+
// Only shrink if parent is NOT scrollable
|
|
488
|
+
// Scrollable containers let content overflow and scroll instead
|
|
489
|
+
kidMain += ((unwrap(layout.flexShrink[fkid]) ?? 1) / totalShrink) * freeSpace
|
|
490
|
+
}
|
|
491
|
+
kidMain = Math.max(0, Math.floor(kidMain))
|
|
492
|
+
|
|
493
|
+
// Apply min/max constraints for main axis
|
|
494
|
+
const minMain = isRow ? unwrap(dimensions.minWidth[fkid]) : unwrap(dimensions.minHeight[fkid])
|
|
495
|
+
const maxMain = isRow ? unwrap(dimensions.maxWidth[fkid]) : unwrap(dimensions.maxHeight[fkid])
|
|
496
|
+
kidMain = clampDim(kidMain, minMain, maxMain, isRow ? contentW : contentH)
|
|
497
|
+
|
|
498
|
+
let kidCross = isRow
|
|
499
|
+
? (eh > 0 ? eh : (alignItems === ALIGN_STRETCH ? crossSize / lineCount : intrinsicH[fkid]!))
|
|
500
|
+
: (ew > 0 ? ew : (alignItems === ALIGN_STRETCH ? crossSize / lineCount : intrinsicW[fkid]!))
|
|
501
|
+
|
|
502
|
+
// Apply min/max constraints for cross axis
|
|
503
|
+
const minCross = isRow ? unwrap(dimensions.minHeight[fkid]) : unwrap(dimensions.minWidth[fkid])
|
|
504
|
+
const maxCross = isRow ? unwrap(dimensions.maxHeight[fkid]) : unwrap(dimensions.maxWidth[fkid])
|
|
505
|
+
kidCross = clampDim(Math.max(0, Math.floor(kidCross)), minCross, maxCross, isRow ? contentH : contentW)
|
|
506
|
+
|
|
507
|
+
itemMain[fkid] = kidMain
|
|
508
|
+
itemCross[fkid] = kidCross
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// STEP 3: Position items
|
|
513
|
+
let crossOffset = 0
|
|
514
|
+
// Ensure minimum lineHeight of 1 to prevent all lines stacking at same position
|
|
515
|
+
// when there are more lines than vertical space
|
|
516
|
+
const lineHeight = lineCount > 0 ? Math.max(1, Math.floor(crossSize / lineCount)) : crossSize
|
|
517
|
+
|
|
518
|
+
for (let li = 0; li < lineCount; li++) {
|
|
519
|
+
const lineIdx = isReverse ? lineCount - 1 - li : li
|
|
520
|
+
const lStart = lineStarts[lineIdx]!
|
|
521
|
+
const lEnd = lineEnds[lineIdx]!
|
|
522
|
+
|
|
523
|
+
let lineMain = 0
|
|
524
|
+
for (let fi = lStart; fi <= lEnd; fi++) {
|
|
525
|
+
const kid = flowKids[fi]!
|
|
526
|
+
// Include margins in line size calculation (CSS box model)
|
|
527
|
+
const mMain = isRow
|
|
528
|
+
? (unwrap(spacing.marginLeft[kid]) ?? 0) + (unwrap(spacing.marginRight[kid]) ?? 0)
|
|
529
|
+
: (unwrap(spacing.marginTop[kid]) ?? 0) + (unwrap(spacing.marginBottom[kid]) ?? 0)
|
|
530
|
+
lineMain += itemMain[kid]! + mMain + gap
|
|
531
|
+
}
|
|
532
|
+
lineMain -= gap
|
|
533
|
+
|
|
534
|
+
let mainOffset = 0
|
|
535
|
+
let itemGap = gap
|
|
536
|
+
const remainingSpace = mainSize - lineMain
|
|
537
|
+
const itemCount = lEnd - lStart + 1
|
|
538
|
+
|
|
539
|
+
switch (justify) {
|
|
540
|
+
case JUSTIFY_CENTER:
|
|
541
|
+
mainOffset = Math.floor(remainingSpace / 2)
|
|
542
|
+
break
|
|
543
|
+
case JUSTIFY_END:
|
|
544
|
+
mainOffset = remainingSpace
|
|
545
|
+
break
|
|
546
|
+
case JUSTIFY_BETWEEN:
|
|
547
|
+
itemGap = itemCount > 1 ? Math.floor(remainingSpace / (itemCount - 1)) + gap : gap
|
|
548
|
+
break
|
|
549
|
+
case JUSTIFY_AROUND: {
|
|
550
|
+
const around = Math.floor(remainingSpace / itemCount)
|
|
551
|
+
mainOffset = Math.floor(around / 2)
|
|
552
|
+
itemGap = around + gap
|
|
553
|
+
break
|
|
554
|
+
}
|
|
555
|
+
case JUSTIFY_EVENLY: {
|
|
556
|
+
const even = Math.floor(remainingSpace / (itemCount + 1))
|
|
557
|
+
mainOffset = even
|
|
558
|
+
itemGap = even + gap
|
|
559
|
+
break
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Always iterate in DOM order - the reversed position calculation
|
|
564
|
+
// handles the visual reversal for row-reverse/column-reverse
|
|
565
|
+
for (let fi = lStart; fi <= lEnd; fi++) {
|
|
566
|
+
const fkid = flowKids[fi]!
|
|
567
|
+
const sizeMain = itemMain[fkid]!
|
|
568
|
+
const sizeCross = itemCross[fkid]!
|
|
569
|
+
|
|
570
|
+
// Read margins for CSS-compliant positioning
|
|
571
|
+
const mTop = unwrap(spacing.marginTop[fkid]) ?? 0
|
|
572
|
+
const mRight = unwrap(spacing.marginRight[fkid]) ?? 0
|
|
573
|
+
const mBottom = unwrap(spacing.marginBottom[fkid]) ?? 0
|
|
574
|
+
const mLeft = unwrap(spacing.marginLeft[fkid]) ?? 0
|
|
575
|
+
|
|
576
|
+
// align-self overrides parent's align-items for individual items
|
|
577
|
+
// alignSelf: 0=auto, 1=stretch, 2=flex-start, 3=center, 4=flex-end, 5=baseline
|
|
578
|
+
// alignItems: 0=stretch, 1=flex-start, 2=center, 3=flex-end
|
|
579
|
+
// When alignSelf != 0, we subtract 1 to map to alignItems values
|
|
580
|
+
const selfAlign = unwrap(layout.alignSelf[fkid]) ?? ALIGN_SELF_AUTO
|
|
581
|
+
const effectiveAlign = selfAlign !== ALIGN_SELF_AUTO ? (selfAlign - 1) : alignItems
|
|
582
|
+
|
|
583
|
+
let crossPos = crossOffset
|
|
584
|
+
switch (effectiveAlign) {
|
|
585
|
+
case ALIGN_CENTER:
|
|
586
|
+
crossPos += Math.floor((lineHeight - sizeCross) / 2)
|
|
587
|
+
break
|
|
588
|
+
case ALIGN_END:
|
|
589
|
+
crossPos += lineHeight - sizeCross
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// CSS Flexbox: margins offset item position and add space between items
|
|
594
|
+
// For row-reverse/column-reverse, position from the end of the axis
|
|
595
|
+
if (isRow) {
|
|
596
|
+
if (dir === FLEX_ROW_REVERSE) {
|
|
597
|
+
// row-reverse: position from right edge
|
|
598
|
+
outX[fkid] = contentX + contentW - mainOffset - sizeMain - mRight
|
|
599
|
+
} else {
|
|
600
|
+
outX[fkid] = contentX + mainOffset + mLeft
|
|
601
|
+
}
|
|
602
|
+
outY[fkid] = contentY + crossPos + mTop
|
|
603
|
+
outW[fkid] = sizeMain
|
|
604
|
+
outH[fkid] = sizeCross
|
|
605
|
+
} else {
|
|
606
|
+
outX[fkid] = contentX + crossPos + mLeft
|
|
607
|
+
if (dir === FLEX_COLUMN_REVERSE) {
|
|
608
|
+
// column-reverse: position from bottom edge
|
|
609
|
+
outY[fkid] = contentY + contentH - mainOffset - sizeMain - mBottom
|
|
610
|
+
} else {
|
|
611
|
+
outY[fkid] = contentY + mainOffset + mTop
|
|
612
|
+
}
|
|
613
|
+
outW[fkid] = sizeCross
|
|
614
|
+
outH[fkid] = sizeMain
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// TEXT WRAPPING: Now that we know the width, recalculate height for TEXT
|
|
618
|
+
// This fixes the intrinsicH=1 assumption - text wraps to actual width
|
|
619
|
+
if (core.componentType[fkid] === ComponentType.TEXT) {
|
|
620
|
+
const content = unwrap(text.textContent[fkid])
|
|
621
|
+
if (content != null) {
|
|
622
|
+
const str = String(content)
|
|
623
|
+
if (str.length > 0) {
|
|
624
|
+
const wrappedHeight = measureTextHeight(str, outW[fkid]!)
|
|
625
|
+
outH[fkid] = Math.max(1, wrappedHeight)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Track max extent inline (zero overhead) - include margins
|
|
631
|
+
if (isRow) {
|
|
632
|
+
childrenMaxMain = Math.max(childrenMaxMain, mainOffset + mLeft + outW[fkid]! + mRight)
|
|
633
|
+
childrenMaxCross = Math.max(childrenMaxCross, crossPos + mTop + outH[fkid]! + mBottom)
|
|
634
|
+
} else {
|
|
635
|
+
childrenMaxMain = Math.max(childrenMaxMain, mainOffset + mTop + outH[fkid]! + mBottom)
|
|
636
|
+
childrenMaxCross = Math.max(childrenMaxCross, crossPos + mLeft + outW[fkid]! + mRight)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Advance mainOffset including margins (CSS box model)
|
|
640
|
+
const mainMargin = isRow ? (mLeft + mRight) : (mTop + mBottom)
|
|
641
|
+
mainOffset += (isRow ? outW[fkid]! : outH[fkid]!) + mainMargin + itemGap
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
crossOffset += lineHeight
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Scroll detection - uses values tracked inline above (no extra loop!)
|
|
648
|
+
// Note: overflow already read above for isScrollableParent check
|
|
649
|
+
if (isScrollableParent) {
|
|
650
|
+
const childrenMaxX = isRow ? childrenMaxMain : childrenMaxCross
|
|
651
|
+
const childrenMaxY = isRow ? childrenMaxCross : childrenMaxMain
|
|
652
|
+
const scrollRangeX = Math.max(0, childrenMaxX - contentW)
|
|
653
|
+
const scrollRangeY = Math.max(0, childrenMaxY - contentH)
|
|
654
|
+
|
|
655
|
+
if (overflow === Overflow.SCROLL || scrollRangeX > 0 || scrollRangeY > 0) {
|
|
656
|
+
// overflow === SCROLL always scrollable; AUTO only if content overflows
|
|
657
|
+
outScrollable[parent] = 1
|
|
658
|
+
outMaxScrollX[parent] = scrollRangeX
|
|
659
|
+
outMaxScrollY[parent] = scrollRangeY
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
665
|
+
// HELPER: Absolute positioning
|
|
666
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
667
|
+
function layoutAbsolute(i: number): void {
|
|
668
|
+
let container = unwrap(core.parentIndex[i]) ?? -1
|
|
669
|
+
while (container >= 0 && indices.has(container)) {
|
|
670
|
+
if ((unwrap(layout.position[container]) ?? POS_RELATIVE) !== POS_RELATIVE) break
|
|
671
|
+
container = unwrap(core.parentIndex[container]) ?? -1
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let containerX = 0, containerY = 0, containerW = outW[0] ?? 80, containerH = outH[0] ?? 24
|
|
675
|
+
if (container >= 0 && indices.has(container)) {
|
|
676
|
+
containerX = outX[container]!
|
|
677
|
+
containerY = outY[container]!
|
|
678
|
+
containerW = outW[container]!
|
|
679
|
+
containerH = outH[container]!
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Resolve dimensions against containing block
|
|
683
|
+
const ew = resolveDim(unwrap(dimensions.width[i]), containerW)
|
|
684
|
+
const eh = resolveDim(unwrap(dimensions.height[i]), containerH)
|
|
685
|
+
let absW = ew > 0 ? ew : intrinsicW[i]!
|
|
686
|
+
let absH = eh > 0 ? eh : intrinsicH[i]!
|
|
687
|
+
|
|
688
|
+
// Apply min/max constraints
|
|
689
|
+
absW = clampDim(absW, unwrap(dimensions.minWidth[i]), unwrap(dimensions.maxWidth[i]), containerW)
|
|
690
|
+
absH = clampDim(absH, unwrap(dimensions.minHeight[i]), unwrap(dimensions.maxHeight[i]), containerH)
|
|
691
|
+
outW[i] = absW
|
|
692
|
+
outH[i] = absH
|
|
693
|
+
|
|
694
|
+
const t = unwrap(layout.top[i])
|
|
695
|
+
const r = unwrap(layout.right[i])
|
|
696
|
+
const b = unwrap(layout.bottom[i])
|
|
697
|
+
const l = unwrap(layout.left[i])
|
|
698
|
+
|
|
699
|
+
if (l !== undefined && l !== 0) {
|
|
700
|
+
outX[i] = containerX + l
|
|
701
|
+
} else if (r !== undefined && r !== 0) {
|
|
702
|
+
outX[i] = containerX + containerW - outW[i]! - r
|
|
703
|
+
} else {
|
|
704
|
+
outX[i] = containerX
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (t !== undefined && t !== 0) {
|
|
708
|
+
outY[i] = containerY + t
|
|
709
|
+
} else if (b !== undefined && b !== 0) {
|
|
710
|
+
outY[i] = containerY + containerH - outH[i]! - b
|
|
711
|
+
} else {
|
|
712
|
+
outY[i] = containerY
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Position the absolute element's own children (non-recursive)
|
|
716
|
+
layoutChildrenOf(i)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
720
|
+
// PASS 4: Layout (top-down, ITERATIVE via BFS order)
|
|
721
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
722
|
+
|
|
723
|
+
// First, position all roots
|
|
724
|
+
// Root elements resolve percentage dimensions against terminal size
|
|
725
|
+
for (let ri = 0; ri < rootCount; ri++) {
|
|
726
|
+
const root = bfsQueue[ri]!
|
|
727
|
+
const rawW = unwrap(dimensions.width[root])
|
|
728
|
+
const rawH = unwrap(dimensions.height[root])
|
|
729
|
+
const ew = resolveDim(rawW, terminalWidth)
|
|
730
|
+
const eh = resolveDim(rawH, terminalHeight)
|
|
731
|
+
|
|
732
|
+
outX[root] = 0
|
|
733
|
+
outY[root] = 0
|
|
734
|
+
outW[root] = ew > 0 ? ew : terminalWidth
|
|
735
|
+
|
|
736
|
+
// Height handling:
|
|
737
|
+
// - If explicit height set, use it
|
|
738
|
+
// - If constrainHeight (fullscreen), use terminal height
|
|
739
|
+
// - If unconstrained (inline/append), use intrinsic height (content determines size)
|
|
740
|
+
if (eh > 0) {
|
|
741
|
+
outH[root] = eh
|
|
742
|
+
} else if (constrainHeight) {
|
|
743
|
+
outH[root] = terminalHeight
|
|
744
|
+
} else {
|
|
745
|
+
// Inline/append: content determines height
|
|
746
|
+
outH[root] = intrinsicH[root] ?? 1
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Then iterate through BFS order - each node positions its children
|
|
751
|
+
// BFS guarantees parents are processed before children, so child positions
|
|
752
|
+
// are always set before we need to process them as parents themselves.
|
|
753
|
+
for (let qi = 0; qi < bfsQueue.length; qi++) {
|
|
754
|
+
layoutChildrenOf(bfsQueue[qi]!)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
758
|
+
// PASS 5: Absolute positioning
|
|
759
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
760
|
+
for (const i of indices) {
|
|
761
|
+
if ((unwrap(layout.position[i]) ?? POS_RELATIVE) === POS_ABSOLUTE) {
|
|
762
|
+
layoutAbsolute(i)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
767
|
+
// Content bounds: Just use root dimensions (root is at 0,0 and contains all)
|
|
768
|
+
// No need for separate pass - we already have the root's size from PASS 4
|
|
769
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
770
|
+
// OLD PASS 6 (iterated all components - unnecessary):
|
|
771
|
+
// let contentWidth = 0
|
|
772
|
+
// let contentHeight = 0
|
|
773
|
+
// for (const i of indices) {
|
|
774
|
+
// const right = (outX[i] ?? 0) + (outW[i] ?? 0)
|
|
775
|
+
// const bottom = (outY[i] ?? 0) + (outH[i] ?? 0)
|
|
776
|
+
// if (right > contentWidth) contentWidth = right
|
|
777
|
+
// if (bottom > contentHeight) contentHeight = bottom
|
|
778
|
+
// }
|
|
779
|
+
|
|
780
|
+
// Simple: root is at (0,0), so content bounds = root dimensions
|
|
781
|
+
const contentWidth = rootCount > 0 ? (outW[bfsQueue[0]!] ?? 0) : 0
|
|
782
|
+
const contentHeight = rootCount > 0 ? (outH[bfsQueue[0]!] ?? 0) : 0
|
|
783
|
+
|
|
784
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
785
|
+
// RETURN
|
|
786
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
787
|
+
return {
|
|
788
|
+
x: outX,
|
|
789
|
+
y: outY,
|
|
790
|
+
width: outW,
|
|
791
|
+
height: outH,
|
|
792
|
+
scrollable: outScrollable,
|
|
793
|
+
maxScrollY: outMaxScrollY,
|
|
794
|
+
maxScrollX: outMaxScrollX,
|
|
795
|
+
contentWidth,
|
|
796
|
+
contentHeight
|
|
797
|
+
}
|
|
798
|
+
}
|