@rlabs-inc/tui 0.1.0 → 0.2.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 +126 -13
- package/index.ts +11 -5
- package/package.json +2 -2
- package/src/api/mount.ts +42 -27
- package/src/engine/arrays/core.ts +13 -21
- package/src/engine/arrays/dimensions.ts +22 -32
- package/src/engine/arrays/index.ts +88 -86
- package/src/engine/arrays/interaction.ts +34 -48
- package/src/engine/arrays/layout.ts +67 -92
- package/src/engine/arrays/spacing.ts +37 -52
- package/src/engine/arrays/text.ts +23 -31
- package/src/engine/arrays/visual.ts +56 -75
- package/src/engine/inheritance.ts +18 -18
- package/src/engine/registry.ts +15 -0
- package/src/pipeline/frameBuffer.ts +26 -26
- package/src/pipeline/layout/index.ts +2 -2
- package/src/pipeline/layout/titan-engine.ts +112 -84
- package/src/primitives/animation.ts +194 -0
- package/src/primitives/box.ts +74 -86
- package/src/primitives/each.ts +87 -0
- package/src/primitives/index.ts +7 -0
- package/src/primitives/scope.ts +215 -0
- package/src/primitives/show.ts +77 -0
- package/src/primitives/text.ts +63 -59
- package/src/primitives/types.ts +1 -1
- package/src/primitives/when.ts +102 -0
- package/src/renderer/append-region.ts +303 -0
- package/src/renderer/index.ts +4 -2
- package/src/renderer/output.ts +11 -34
- package/src/state/focus.ts +16 -5
- package/src/state/global-keys.ts +184 -0
- package/src/state/index.ts +44 -8
- package/src/state/input.ts +534 -0
- package/src/state/keyboard.ts +98 -674
- package/src/state/mouse.ts +163 -340
- package/src/state/scroll.ts +7 -9
- package/src/types/index.ts +6 -0
- package/src/renderer/input.ts +0 -518
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* HitGrid updates are returned as data to be applied by the render effect.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { derived,
|
|
19
|
+
import { derived, neverEquals } from '@rlabs-inc/signals'
|
|
20
20
|
import type { FrameBuffer, RGBA } from '../types'
|
|
21
21
|
import { ComponentType } from '../types'
|
|
22
22
|
import { Colors, TERMINAL_DEFAULT, rgbaBlend, rgbaLerp } from '../types/color'
|
|
@@ -46,10 +46,10 @@ import {
|
|
|
46
46
|
// Import arrays
|
|
47
47
|
import * as core from '../engine/arrays/core'
|
|
48
48
|
import * as visual from '../engine/arrays/visual'
|
|
49
|
-
import *
|
|
50
|
-
import *
|
|
51
|
-
import *
|
|
52
|
-
import *
|
|
49
|
+
import *as text from '../engine/arrays/text'
|
|
50
|
+
import *as spacing from '../engine/arrays/spacing'
|
|
51
|
+
import *as layout from '../engine/arrays/layout'
|
|
52
|
+
import *as interaction from '../engine/arrays/interaction'
|
|
53
53
|
|
|
54
54
|
// Import layout derived
|
|
55
55
|
import { layoutDerived, terminalWidth, terminalHeight, renderMode } from './layout'
|
|
@@ -119,10 +119,10 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
|
|
|
119
119
|
|
|
120
120
|
for (const i of indices) {
|
|
121
121
|
if (core.componentType[i] === ComponentType.NONE) continue
|
|
122
|
-
const vis =
|
|
122
|
+
const vis = core.visible[i]
|
|
123
123
|
if (vis === 0 || vis === false) continue
|
|
124
124
|
|
|
125
|
-
const parent =
|
|
125
|
+
const parent = core.parentIndex[i] ?? -1
|
|
126
126
|
if (parent === -1) {
|
|
127
127
|
rootIndices.push(i)
|
|
128
128
|
} else {
|
|
@@ -136,11 +136,11 @@ export const frameBufferDerived = derived((): FrameBufferResult => {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
// Sort roots by zIndex
|
|
139
|
-
rootIndices.sort((a, b) => (
|
|
139
|
+
rootIndices.sort((a, b) => (layout.zIndex[a] || 0) - (layout.zIndex[b] || 0))
|
|
140
140
|
|
|
141
141
|
// Sort children by zIndex
|
|
142
142
|
for (const children of childMap.values()) {
|
|
143
|
-
children.sort((a, b) => (
|
|
143
|
+
children.sort((a, b) => (layout.zIndex[a] || 0) - (layout.zIndex[b] || 0))
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Render tree recursively
|
|
@@ -180,7 +180,7 @@ function renderComponent(
|
|
|
180
180
|
parentScrollX: number
|
|
181
181
|
): void {
|
|
182
182
|
// Skip invisible/invalid components
|
|
183
|
-
const vis =
|
|
183
|
+
const vis = core.visible[index]
|
|
184
184
|
if (vis === 0 || vis === false) return
|
|
185
185
|
if (core.componentType[index] === ComponentType.NONE) return
|
|
186
186
|
|
|
@@ -239,10 +239,10 @@ function renderComponent(
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// Calculate content area (inside borders and padding)
|
|
242
|
-
const padTop = (
|
|
243
|
-
const padRight = (
|
|
244
|
-
const padBottom = (
|
|
245
|
-
const padLeft = (
|
|
242
|
+
const padTop = (spacing.paddingTop[index] || 0) + (hasAnyBorder && borderStyles.top > 0 ? 1 : 0)
|
|
243
|
+
const padRight = (spacing.paddingRight[index] || 0) + (hasAnyBorder && borderStyles.right > 0 ? 1 : 0)
|
|
244
|
+
const padBottom = (spacing.paddingBottom[index] || 0) + (hasAnyBorder && borderStyles.bottom > 0 ? 1 : 0)
|
|
245
|
+
const padLeft = (spacing.paddingLeft[index] || 0) + (hasAnyBorder && borderStyles.left > 0 ? 1 : 0)
|
|
246
246
|
|
|
247
247
|
const contentX = x + padLeft
|
|
248
248
|
const contentY = y + padTop
|
|
@@ -292,8 +292,8 @@ function renderComponent(
|
|
|
292
292
|
|
|
293
293
|
// Get this component's scroll offset (scrollable comes from layout, offset from interaction)
|
|
294
294
|
const isScrollable = (computedLayout.scrollable[index] ?? 0) === 1
|
|
295
|
-
const scrollY = isScrollable ? (
|
|
296
|
-
const scrollX = isScrollable ? (
|
|
295
|
+
const scrollY = isScrollable ? (interaction.scrollOffsetY[index] || 0) : 0
|
|
296
|
+
const scrollX = isScrollable ? (interaction.scrollOffsetX[index] || 0) : 0
|
|
297
297
|
|
|
298
298
|
// Accumulated scroll for children
|
|
299
299
|
const childScrollY = parentScrollY + scrollY
|
|
@@ -331,13 +331,13 @@ function renderText(
|
|
|
331
331
|
fg: RGBA,
|
|
332
332
|
clip: ClipRect
|
|
333
333
|
): void {
|
|
334
|
+
// Read through slotArray proxy - same pattern as color reads in inheritance.ts
|
|
334
335
|
const rawValue = text.textContent[index]
|
|
335
|
-
const
|
|
336
|
-
const content = unwrapped == null ? '' : String(unwrapped)
|
|
336
|
+
const content = rawValue == null ? '' : String(rawValue)
|
|
337
337
|
if (!content) return
|
|
338
338
|
|
|
339
|
-
const attrs =
|
|
340
|
-
const align =
|
|
339
|
+
const attrs = text.textAttrs[index] || 0
|
|
340
|
+
const align = text.textAlign[index] || 0
|
|
341
341
|
|
|
342
342
|
// Word wrap the text
|
|
343
343
|
const lines = wrapText(content, w)
|
|
@@ -376,9 +376,9 @@ function renderInput(
|
|
|
376
376
|
fg: RGBA,
|
|
377
377
|
clip: ClipRect
|
|
378
378
|
): void {
|
|
379
|
-
const content =
|
|
380
|
-
const attrs =
|
|
381
|
-
const cursorPos =
|
|
379
|
+
const content = text.textContent[index] || '' // SlotArray auto-unwraps
|
|
380
|
+
const attrs = text.textAttrs[index] || 0
|
|
381
|
+
const cursorPos = interaction.cursorPosition[index] || 0
|
|
382
382
|
|
|
383
383
|
if (w <= 0) return
|
|
384
384
|
|
|
@@ -427,7 +427,7 @@ function renderProgress(
|
|
|
427
427
|
fg: RGBA,
|
|
428
428
|
clip?: ClipRect
|
|
429
429
|
): void {
|
|
430
|
-
const valueStr =
|
|
430
|
+
const valueStr = text.textContent[index] || '0' // SlotArray auto-unwraps
|
|
431
431
|
const progress = Math.max(0, Math.min(1, parseFloat(valueStr) || 0))
|
|
432
432
|
const filled = Math.round(progress * w)
|
|
433
433
|
|
|
@@ -458,8 +458,8 @@ function renderSelect(
|
|
|
458
458
|
fg: RGBA,
|
|
459
459
|
clip: ClipRect
|
|
460
460
|
): void {
|
|
461
|
-
const content =
|
|
462
|
-
const attrs =
|
|
461
|
+
const content = text.textContent[index] || '' // SlotArray auto-unwraps
|
|
462
|
+
const attrs = text.textAttrs[index] || 0
|
|
463
463
|
|
|
464
464
|
// For now, just show current selection
|
|
465
465
|
const displayText = truncateText(content, w - 2) // Leave room for dropdown indicator
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Return plain output arrays
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { derived, signal } from '@rlabs-inc/signals'
|
|
18
|
+
import { derived, signal, neverEquals } from '@rlabs-inc/signals'
|
|
19
19
|
import { getAllocatedIndices } from '../../engine/registry'
|
|
20
20
|
import { computeLayoutTitan, resetTitanArrays } from './titan-engine'
|
|
21
21
|
import type { ComputedLayout } from './types'
|
|
@@ -87,7 +87,7 @@ export const layoutDerived = derived((): ComputedLayout => {
|
|
|
87
87
|
// TITAN ENGINE: Read arrays, compute, return.
|
|
88
88
|
// Reactivity tracks dependencies as we read - no manual tracking needed.
|
|
89
89
|
return computeLayoutTitan(tw, th, indices, constrainHeight)
|
|
90
|
-
})
|
|
90
|
+
}, { equals: neverEquals })
|
|
91
91
|
|
|
92
92
|
// =============================================================================
|
|
93
93
|
// EXPORTS
|
|
@@ -206,10 +206,10 @@ export function computeLayoutTitan(
|
|
|
206
206
|
|
|
207
207
|
for (const i of indices) {
|
|
208
208
|
// Skip invisible components - they don't participate in layout
|
|
209
|
-
const vis =
|
|
209
|
+
const vis = core.visible[i]
|
|
210
210
|
if (vis === 0 || vis === false) continue
|
|
211
211
|
|
|
212
|
-
const parent =
|
|
212
|
+
const parent = core.parentIndex[i] ?? -1
|
|
213
213
|
|
|
214
214
|
if (parent >= 0 && indices.has(parent)) {
|
|
215
215
|
if (firstChild[parent] === -1) {
|
|
@@ -244,32 +244,32 @@ export function computeLayoutTitan(
|
|
|
244
244
|
const type = core.componentType[i]
|
|
245
245
|
|
|
246
246
|
if (type === ComponentType.TEXT) {
|
|
247
|
-
const content =
|
|
247
|
+
const content = text.textContent[i] // SlotArray auto-unwraps & tracks
|
|
248
248
|
// Check for null/undefined, NOT truthiness (0 and '' are valid content!)
|
|
249
249
|
if (content != null) {
|
|
250
250
|
const str = String(content)
|
|
251
251
|
|
|
252
252
|
if (str.length > 0) {
|
|
253
253
|
// TEXT WRAPPING: Calculate available width for height measurement
|
|
254
|
-
const parentIdx =
|
|
254
|
+
const parentIdx = core.parentIndex[i] ?? -1
|
|
255
255
|
let availableW = terminalWidth
|
|
256
256
|
|
|
257
257
|
if (parentIdx >= 0) {
|
|
258
|
-
const rawParentW =
|
|
258
|
+
const rawParentW = dimensions.width[parentIdx]
|
|
259
259
|
const parentExplicitW = typeof rawParentW === 'number' ? rawParentW : 0
|
|
260
260
|
if (parentExplicitW > 0) {
|
|
261
|
-
const pPadL =
|
|
262
|
-
const pPadR =
|
|
263
|
-
const pBorderStyle =
|
|
264
|
-
const pBorderL = pBorderStyle > 0 || (
|
|
265
|
-
const pBorderR = pBorderStyle > 0 || (
|
|
261
|
+
const pPadL = spacing.paddingLeft[parentIdx] ?? 0
|
|
262
|
+
const pPadR = spacing.paddingRight[parentIdx] ?? 0
|
|
263
|
+
const pBorderStyle = visual.borderStyle[parentIdx] ?? 0
|
|
264
|
+
const pBorderL = pBorderStyle > 0 || (visual.borderLeft[parentIdx] ?? 0) > 0 ? 1 : 0
|
|
265
|
+
const pBorderR = pBorderStyle > 0 || (visual.borderRight[parentIdx] ?? 0) > 0 ? 1 : 0
|
|
266
266
|
availableW = Math.max(1, parentExplicitW - pPadL - pPadR - pBorderL - pBorderR)
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
// CACHE CHECK: Hash text content, compare with cached
|
|
271
271
|
// Only recompute stringWidth/measureTextHeight if content or availableW changed
|
|
272
|
-
const textHash = Bun.hash(str)
|
|
272
|
+
const textHash = BigInt(Bun.hash(str))
|
|
273
273
|
if (textHash === cachedTextHash[i] && availableW === cachedAvailW[i]) {
|
|
274
274
|
// Cache hit - reuse cached intrinsics (skip expensive computation!)
|
|
275
275
|
intrinsicW[i] = cachedIntrinsicW[i]!
|
|
@@ -290,11 +290,17 @@ export function computeLayoutTitan(
|
|
|
290
290
|
}
|
|
291
291
|
} else {
|
|
292
292
|
// BOX/Container - calculate intrinsic from children + padding + borders
|
|
293
|
+
// EXCEPTION: Scrollable containers should have minimal intrinsic height
|
|
294
|
+
// so they don't force parents to expand - content scrolls instead
|
|
295
|
+
const overflow = layout.overflow[i] ?? Overflow.VISIBLE
|
|
296
|
+
const isScrollable = overflow === Overflow.SCROLL || overflow === Overflow.AUTO
|
|
297
|
+
|
|
293
298
|
let kid = firstChild[i]!
|
|
294
|
-
if (kid !== -1) {
|
|
295
|
-
|
|
299
|
+
if (kid !== -1 && !isScrollable) {
|
|
300
|
+
// Normal containers: intrinsic size includes all children
|
|
301
|
+
const dir = layout.flexDirection[i] ?? FLEX_COLUMN
|
|
296
302
|
const isRow = dir === FLEX_ROW || dir === FLEX_ROW_REVERSE
|
|
297
|
-
const gap =
|
|
303
|
+
const gap = spacing.gap[i] ?? 0
|
|
298
304
|
|
|
299
305
|
let sumMain = 0
|
|
300
306
|
let maxCross = 0
|
|
@@ -306,8 +312,8 @@ export function computeLayoutTitan(
|
|
|
306
312
|
// This ensures children with explicit sizes contribute correctly
|
|
307
313
|
// Note: Percentage dimensions (strings) → 0 for intrinsic calculation
|
|
308
314
|
// They'll be resolved against parent computed size in layout phase
|
|
309
|
-
const rawKidW =
|
|
310
|
-
const rawKidH =
|
|
315
|
+
const rawKidW = dimensions.width[kid]
|
|
316
|
+
const rawKidH = dimensions.height[kid]
|
|
311
317
|
const kidExplicitW = typeof rawKidW === 'number' ? rawKidW : 0
|
|
312
318
|
const kidExplicitH = typeof rawKidH === 'number' ? rawKidH : 0
|
|
313
319
|
const kidW = kidExplicitW > 0 ? kidExplicitW : intrinsicW[kid]!
|
|
@@ -318,19 +324,24 @@ export function computeLayoutTitan(
|
|
|
318
324
|
// Adding borders here was DOUBLE-COUNTING and inflating contentHeight.
|
|
319
325
|
//
|
|
320
326
|
// OLD CODE (double-counted borders):
|
|
321
|
-
// const kidBs =
|
|
322
|
-
// const kidBordT = kidBs > 0 || (
|
|
323
|
-
// const kidBordB = kidBs > 0 || (
|
|
324
|
-
// const kidBordL = kidBs > 0 || (
|
|
325
|
-
// const kidBordR = kidBs > 0 || (
|
|
327
|
+
// const kidBs = visual.borderStyle[kid] ?? 0
|
|
328
|
+
// const kidBordT = kidBs > 0 || (visual.borderTop[kid] ?? 0) > 0 ? 1 : 0
|
|
329
|
+
// const kidBordB = kidBs > 0 || (visual.borderBottom[kid] ?? 0) > 0 ? 1 : 0
|
|
330
|
+
// const kidBordL = kidBs > 0 || (visual.borderLeft[kid] ?? 0) > 0 ? 1 : 0
|
|
331
|
+
// const kidBordR = kidBs > 0 || (visual.borderRight[kid] ?? 0) > 0 ? 1 : 0
|
|
326
332
|
// const kidTotalW = kidW + kidBordL + kidBordR
|
|
327
333
|
// const kidTotalH = kidH + kidBordT + kidBordB
|
|
328
334
|
|
|
335
|
+
// Include child margins in intrinsic size (matches layout pass behavior)
|
|
336
|
+
const kidMarginMain = isRow
|
|
337
|
+
? (spacing.marginLeft[kid] ?? 0) + (spacing.marginRight[kid] ?? 0)
|
|
338
|
+
: (spacing.marginTop[kid] ?? 0) + (spacing.marginBottom[kid] ?? 0)
|
|
339
|
+
|
|
329
340
|
if (isRow) {
|
|
330
|
-
sumMain += kidW + gap
|
|
341
|
+
sumMain += kidW + kidMarginMain + gap
|
|
331
342
|
maxCross = Math.max(maxCross, kidH)
|
|
332
343
|
} else {
|
|
333
|
-
sumMain += kidH + gap
|
|
344
|
+
sumMain += kidH + kidMarginMain + gap
|
|
334
345
|
maxCross = Math.max(maxCross, kidW)
|
|
335
346
|
}
|
|
336
347
|
kid = nextSibling[kid]!
|
|
@@ -339,15 +350,15 @@ export function computeLayoutTitan(
|
|
|
339
350
|
if (childCount > 0) sumMain -= gap
|
|
340
351
|
|
|
341
352
|
// Add padding and borders to intrinsic size
|
|
342
|
-
const padTop =
|
|
343
|
-
const padRight =
|
|
344
|
-
const padBottom =
|
|
345
|
-
const padLeft =
|
|
346
|
-
const borderStyle =
|
|
347
|
-
const borderT = borderStyle > 0 || (
|
|
348
|
-
const borderR = borderStyle > 0 || (
|
|
349
|
-
const borderB = borderStyle > 0 || (
|
|
350
|
-
const borderL = borderStyle > 0 || (
|
|
353
|
+
const padTop = spacing.paddingTop[i] ?? 0
|
|
354
|
+
const padRight = spacing.paddingRight[i] ?? 0
|
|
355
|
+
const padBottom = spacing.paddingBottom[i] ?? 0
|
|
356
|
+
const padLeft = spacing.paddingLeft[i] ?? 0
|
|
357
|
+
const borderStyle = visual.borderStyle[i] ?? 0
|
|
358
|
+
const borderT = borderStyle > 0 || (visual.borderTop[i] ?? 0) > 0 ? 1 : 0
|
|
359
|
+
const borderR = borderStyle > 0 || (visual.borderRight[i] ?? 0) > 0 ? 1 : 0
|
|
360
|
+
const borderB = borderStyle > 0 || (visual.borderBottom[i] ?? 0) > 0 ? 1 : 0
|
|
361
|
+
const borderL = borderStyle > 0 || (visual.borderLeft[i] ?? 0) > 0 ? 1 : 0
|
|
351
362
|
|
|
352
363
|
const extraWidth = padLeft + padRight + borderL + borderR
|
|
353
364
|
const extraHeight = padTop + padBottom + borderT + borderB
|
|
@@ -359,6 +370,21 @@ export function computeLayoutTitan(
|
|
|
359
370
|
intrinsicW[i] = maxCross + extraWidth
|
|
360
371
|
intrinsicH[i] = sumMain + extraHeight
|
|
361
372
|
}
|
|
373
|
+
} else if (isScrollable) {
|
|
374
|
+
// Scrollable containers: minimal intrinsic size (just padding + borders)
|
|
375
|
+
// Children will overflow and be scrollable, not force container to expand
|
|
376
|
+
const padTop = spacing.paddingTop[i] ?? 0
|
|
377
|
+
const padRight = spacing.paddingRight[i] ?? 0
|
|
378
|
+
const padBottom = spacing.paddingBottom[i] ?? 0
|
|
379
|
+
const padLeft = spacing.paddingLeft[i] ?? 0
|
|
380
|
+
const borderStyle = visual.borderStyle[i] ?? 0
|
|
381
|
+
const borderT = borderStyle > 0 || (visual.borderTop[i] ?? 0) > 0 ? 1 : 0
|
|
382
|
+
const borderR = borderStyle > 0 || (visual.borderRight[i] ?? 0) > 0 ? 1 : 0
|
|
383
|
+
const borderB = borderStyle > 0 || (visual.borderBottom[i] ?? 0) > 0 ? 1 : 0
|
|
384
|
+
const borderL = borderStyle > 0 || (visual.borderLeft[i] ?? 0) > 0 ? 1 : 0
|
|
385
|
+
|
|
386
|
+
intrinsicW[i] = padLeft + padRight + borderL + borderR
|
|
387
|
+
intrinsicH[i] = padTop + padBottom + borderT + borderB
|
|
362
388
|
}
|
|
363
389
|
}
|
|
364
390
|
}
|
|
@@ -381,7 +407,7 @@ export function computeLayoutTitan(
|
|
|
381
407
|
// Collect flow children
|
|
382
408
|
let kid = firstChild[parent]!
|
|
383
409
|
while (kid !== -1) {
|
|
384
|
-
if ((
|
|
410
|
+
if ((layout.position[kid] ?? POS_RELATIVE) !== POS_ABSOLUTE) {
|
|
385
411
|
flowKids.push(kid)
|
|
386
412
|
}
|
|
387
413
|
kid = nextSibling[kid]!
|
|
@@ -390,16 +416,16 @@ export function computeLayoutTitan(
|
|
|
390
416
|
if (flowKids.length === 0) return
|
|
391
417
|
|
|
392
418
|
// Parent's content area
|
|
393
|
-
const pPadT =
|
|
394
|
-
const pPadR =
|
|
395
|
-
const pPadB =
|
|
396
|
-
const pPadL =
|
|
419
|
+
const pPadT = spacing.paddingTop[parent] ?? 0
|
|
420
|
+
const pPadR = spacing.paddingRight[parent] ?? 0
|
|
421
|
+
const pPadB = spacing.paddingBottom[parent] ?? 0
|
|
422
|
+
const pPadL = spacing.paddingLeft[parent] ?? 0
|
|
397
423
|
|
|
398
|
-
const pBs =
|
|
399
|
-
const pBordT = pBs > 0 || (
|
|
400
|
-
const pBordR = pBs > 0 || (
|
|
401
|
-
const pBordB = pBs > 0 || (
|
|
402
|
-
const pBordL = pBs > 0 || (
|
|
424
|
+
const pBs = visual.borderStyle[parent] ?? 0
|
|
425
|
+
const pBordT = pBs > 0 || (visual.borderTop[parent] ?? 0) > 0 ? 1 : 0
|
|
426
|
+
const pBordR = pBs > 0 || (visual.borderRight[parent] ?? 0) > 0 ? 1 : 0
|
|
427
|
+
const pBordB = pBs > 0 || (visual.borderBottom[parent] ?? 0) > 0 ? 1 : 0
|
|
428
|
+
const pBordL = pBs > 0 || (visual.borderLeft[parent] ?? 0) > 0 ? 1 : 0
|
|
403
429
|
|
|
404
430
|
const contentX = outX[parent]! + pPadL + pBordL
|
|
405
431
|
const contentY = outY[parent]! + pPadT + pBordT
|
|
@@ -407,17 +433,19 @@ export function computeLayoutTitan(
|
|
|
407
433
|
const contentH = Math.max(0, outH[parent]! - pPadT - pPadB - pBordT - pBordB)
|
|
408
434
|
|
|
409
435
|
// Flex properties
|
|
410
|
-
const dir =
|
|
411
|
-
const wrap =
|
|
412
|
-
const justify =
|
|
413
|
-
const alignItems =
|
|
414
|
-
const gap =
|
|
415
|
-
const overflow =
|
|
436
|
+
const dir = layout.flexDirection[parent] ?? FLEX_COLUMN
|
|
437
|
+
const wrap = layout.flexWrap[parent] ?? WRAP_NOWRAP
|
|
438
|
+
const justify = layout.justifyContent[parent] ?? JUSTIFY_START
|
|
439
|
+
const alignItems = layout.alignItems[parent] ?? ALIGN_STRETCH
|
|
440
|
+
const gap = spacing.gap[parent] ?? 0
|
|
441
|
+
const overflow = layout.overflow[parent] ?? Overflow.VISIBLE
|
|
416
442
|
|
|
417
443
|
const isRow = dir === FLEX_ROW || dir === FLEX_ROW_REVERSE
|
|
418
444
|
const isReverse = dir === FLEX_ROW_REVERSE || dir === FLEX_COLUMN_REVERSE
|
|
419
445
|
// Scrollable containers should NOT shrink children - content scrolls instead
|
|
420
|
-
|
|
446
|
+
// In fullscreen mode, root boxes (parentIndex === -1) are auto-scrollable
|
|
447
|
+
const isRoot = (core.parentIndex[parent] ?? -1) < 0
|
|
448
|
+
const isScrollableParent = overflow === Overflow.SCROLL || overflow === Overflow.AUTO || (isRoot && constrainHeight)
|
|
421
449
|
|
|
422
450
|
const mainSize = isRow ? contentW : contentH
|
|
423
451
|
const crossSize = isRow ? contentH : contentW
|
|
@@ -429,8 +457,8 @@ export function computeLayoutTitan(
|
|
|
429
457
|
|
|
430
458
|
for (let fi = 0; fi < flowKids.length; fi++) {
|
|
431
459
|
const fkid = flowKids[fi]!
|
|
432
|
-
const ew = resolveDim(
|
|
433
|
-
const eh = resolveDim(
|
|
460
|
+
const ew = resolveDim(dimensions.width[fkid], contentW)
|
|
461
|
+
const eh = resolveDim(dimensions.height[fkid], contentH)
|
|
434
462
|
const kidMain = isRow
|
|
435
463
|
? (ew > 0 ? ew : intrinsicW[fkid]!)
|
|
436
464
|
: (eh > 0 ? eh : intrinsicH[fkid]!)
|
|
@@ -464,17 +492,17 @@ export function computeLayoutTitan(
|
|
|
464
492
|
|
|
465
493
|
for (let fi = lStart; fi <= lEnd; fi++) {
|
|
466
494
|
const fkid = flowKids[fi]!
|
|
467
|
-
totalGrow +=
|
|
468
|
-
totalShrink +=
|
|
495
|
+
totalGrow += layout.flexGrow[fkid] ?? 0
|
|
496
|
+
totalShrink += layout.flexShrink[fkid] ?? 1
|
|
469
497
|
}
|
|
470
498
|
|
|
471
499
|
for (let fi = lStart; fi <= lEnd; fi++) {
|
|
472
500
|
const fkid = flowKids[fi]!
|
|
473
|
-
const ew = resolveDim(
|
|
474
|
-
const eh = resolveDim(
|
|
501
|
+
const ew = resolveDim(dimensions.width[fkid], contentW)
|
|
502
|
+
const eh = resolveDim(dimensions.height[fkid], contentH)
|
|
475
503
|
|
|
476
504
|
// flex-basis takes priority over width/height for main axis size
|
|
477
|
-
const basis =
|
|
505
|
+
const basis = layout.flexBasis[fkid] ?? 0
|
|
478
506
|
let kidMain = basis > 0
|
|
479
507
|
? basis
|
|
480
508
|
: (isRow
|
|
@@ -482,17 +510,17 @@ export function computeLayoutTitan(
|
|
|
482
510
|
: (eh > 0 ? eh : intrinsicH[fkid]!))
|
|
483
511
|
|
|
484
512
|
if (freeSpace > 0 && totalGrow > 0) {
|
|
485
|
-
kidMain += ((
|
|
513
|
+
kidMain += ((layout.flexGrow[fkid] ?? 0) / totalGrow) * freeSpace
|
|
486
514
|
} else if (freeSpace < 0 && totalShrink > 0 && !isScrollableParent) {
|
|
487
515
|
// Only shrink if parent is NOT scrollable
|
|
488
516
|
// Scrollable containers let content overflow and scroll instead
|
|
489
|
-
kidMain += ((
|
|
517
|
+
kidMain += ((layout.flexShrink[fkid] ?? 1) / totalShrink) * freeSpace
|
|
490
518
|
}
|
|
491
519
|
kidMain = Math.max(0, Math.floor(kidMain))
|
|
492
520
|
|
|
493
521
|
// Apply min/max constraints for main axis
|
|
494
|
-
const minMain = isRow ?
|
|
495
|
-
const maxMain = isRow ?
|
|
522
|
+
const minMain = isRow ? dimensions.minWidth[fkid] : dimensions.minHeight[fkid]
|
|
523
|
+
const maxMain = isRow ? dimensions.maxWidth[fkid] : dimensions.maxHeight[fkid]
|
|
496
524
|
kidMain = clampDim(kidMain, minMain, maxMain, isRow ? contentW : contentH)
|
|
497
525
|
|
|
498
526
|
let kidCross = isRow
|
|
@@ -500,8 +528,8 @@ export function computeLayoutTitan(
|
|
|
500
528
|
: (ew > 0 ? ew : (alignItems === ALIGN_STRETCH ? crossSize / lineCount : intrinsicW[fkid]!))
|
|
501
529
|
|
|
502
530
|
// Apply min/max constraints for cross axis
|
|
503
|
-
const minCross = isRow ?
|
|
504
|
-
const maxCross = isRow ?
|
|
531
|
+
const minCross = isRow ? dimensions.minHeight[fkid] : dimensions.minWidth[fkid]
|
|
532
|
+
const maxCross = isRow ? dimensions.maxHeight[fkid] : dimensions.maxWidth[fkid]
|
|
505
533
|
kidCross = clampDim(Math.max(0, Math.floor(kidCross)), minCross, maxCross, isRow ? contentH : contentW)
|
|
506
534
|
|
|
507
535
|
itemMain[fkid] = kidMain
|
|
@@ -525,8 +553,8 @@ export function computeLayoutTitan(
|
|
|
525
553
|
const kid = flowKids[fi]!
|
|
526
554
|
// Include margins in line size calculation (CSS box model)
|
|
527
555
|
const mMain = isRow
|
|
528
|
-
? (
|
|
529
|
-
: (
|
|
556
|
+
? (spacing.marginLeft[kid] ?? 0) + (spacing.marginRight[kid] ?? 0)
|
|
557
|
+
: (spacing.marginTop[kid] ?? 0) + (spacing.marginBottom[kid] ?? 0)
|
|
530
558
|
lineMain += itemMain[kid]! + mMain + gap
|
|
531
559
|
}
|
|
532
560
|
lineMain -= gap
|
|
@@ -568,16 +596,16 @@ export function computeLayoutTitan(
|
|
|
568
596
|
const sizeCross = itemCross[fkid]!
|
|
569
597
|
|
|
570
598
|
// Read margins for CSS-compliant positioning
|
|
571
|
-
const mTop =
|
|
572
|
-
const mRight =
|
|
573
|
-
const mBottom =
|
|
574
|
-
const mLeft =
|
|
599
|
+
const mTop = spacing.marginTop[fkid] ?? 0
|
|
600
|
+
const mRight = spacing.marginRight[fkid] ?? 0
|
|
601
|
+
const mBottom = spacing.marginBottom[fkid] ?? 0
|
|
602
|
+
const mLeft = spacing.marginLeft[fkid] ?? 0
|
|
575
603
|
|
|
576
604
|
// align-self overrides parent's align-items for individual items
|
|
577
605
|
// alignSelf: 0=auto, 1=stretch, 2=flex-start, 3=center, 4=flex-end, 5=baseline
|
|
578
606
|
// alignItems: 0=stretch, 1=flex-start, 2=center, 3=flex-end
|
|
579
607
|
// When alignSelf != 0, we subtract 1 to map to alignItems values
|
|
580
|
-
const selfAlign =
|
|
608
|
+
const selfAlign = layout.alignSelf[fkid] ?? ALIGN_SELF_AUTO
|
|
581
609
|
const effectiveAlign = selfAlign !== ALIGN_SELF_AUTO ? (selfAlign - 1) : alignItems
|
|
582
610
|
|
|
583
611
|
let crossPos = crossOffset
|
|
@@ -617,7 +645,7 @@ export function computeLayoutTitan(
|
|
|
617
645
|
// TEXT WRAPPING: Now that we know the width, recalculate height for TEXT
|
|
618
646
|
// This fixes the intrinsicH=1 assumption - text wraps to actual width
|
|
619
647
|
if (core.componentType[fkid] === ComponentType.TEXT) {
|
|
620
|
-
const content =
|
|
648
|
+
const content = text.textContent[fkid] // SlotArray auto-unwraps & tracks
|
|
621
649
|
if (content != null) {
|
|
622
650
|
const str = String(content)
|
|
623
651
|
if (str.length > 0) {
|
|
@@ -665,10 +693,10 @@ export function computeLayoutTitan(
|
|
|
665
693
|
// HELPER: Absolute positioning
|
|
666
694
|
// ─────────────────────────────────────────────────────────────────────────
|
|
667
695
|
function layoutAbsolute(i: number): void {
|
|
668
|
-
let container =
|
|
696
|
+
let container = core.parentIndex[i] ?? -1
|
|
669
697
|
while (container >= 0 && indices.has(container)) {
|
|
670
|
-
if ((
|
|
671
|
-
container =
|
|
698
|
+
if ((layout.position[container] ?? POS_RELATIVE) !== POS_RELATIVE) break
|
|
699
|
+
container = core.parentIndex[container] ?? -1
|
|
672
700
|
}
|
|
673
701
|
|
|
674
702
|
let containerX = 0, containerY = 0, containerW = outW[0] ?? 80, containerH = outH[0] ?? 24
|
|
@@ -680,21 +708,21 @@ export function computeLayoutTitan(
|
|
|
680
708
|
}
|
|
681
709
|
|
|
682
710
|
// Resolve dimensions against containing block
|
|
683
|
-
const ew = resolveDim(
|
|
684
|
-
const eh = resolveDim(
|
|
711
|
+
const ew = resolveDim(dimensions.width[i], containerW)
|
|
712
|
+
const eh = resolveDim(dimensions.height[i], containerH)
|
|
685
713
|
let absW = ew > 0 ? ew : intrinsicW[i]!
|
|
686
714
|
let absH = eh > 0 ? eh : intrinsicH[i]!
|
|
687
715
|
|
|
688
716
|
// Apply min/max constraints
|
|
689
|
-
absW = clampDim(absW,
|
|
690
|
-
absH = clampDim(absH,
|
|
717
|
+
absW = clampDim(absW, dimensions.minWidth[i], dimensions.maxWidth[i], containerW)
|
|
718
|
+
absH = clampDim(absH, dimensions.minHeight[i], dimensions.maxHeight[i], containerH)
|
|
691
719
|
outW[i] = absW
|
|
692
720
|
outH[i] = absH
|
|
693
721
|
|
|
694
|
-
const t =
|
|
695
|
-
const r =
|
|
696
|
-
const b =
|
|
697
|
-
const l =
|
|
722
|
+
const t = layout.top[i]
|
|
723
|
+
const r = layout.right[i]
|
|
724
|
+
const b = layout.bottom[i]
|
|
725
|
+
const l = layout.left[i]
|
|
698
726
|
|
|
699
727
|
if (l !== undefined && l !== 0) {
|
|
700
728
|
outX[i] = containerX + l
|
|
@@ -724,8 +752,8 @@ export function computeLayoutTitan(
|
|
|
724
752
|
// Root elements resolve percentage dimensions against terminal size
|
|
725
753
|
for (let ri = 0; ri < rootCount; ri++) {
|
|
726
754
|
const root = bfsQueue[ri]!
|
|
727
|
-
const rawW =
|
|
728
|
-
const rawH =
|
|
755
|
+
const rawW = dimensions.width[root]
|
|
756
|
+
const rawH = dimensions.height[root]
|
|
729
757
|
const ew = resolveDim(rawW, terminalWidth)
|
|
730
758
|
const eh = resolveDim(rawH, terminalHeight)
|
|
731
759
|
|
|
@@ -758,7 +786,7 @@ export function computeLayoutTitan(
|
|
|
758
786
|
// PASS 5: Absolute positioning
|
|
759
787
|
// ─────────────────────────────────────────────────────────────────────────
|
|
760
788
|
for (const i of indices) {
|
|
761
|
-
if ((
|
|
789
|
+
if ((layout.position[i] ?? POS_RELATIVE) === POS_ABSOLUTE) {
|
|
762
790
|
layoutAbsolute(i)
|
|
763
791
|
}
|
|
764
792
|
}
|