@cel-tui/core 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/package.json +3 -2
- package/src/cel.ts +250 -36
- package/src/emitter.ts +84 -49
- package/src/hit-test.ts +65 -12
- package/src/index.ts +3 -1
- package/src/keys.ts +181 -74
- package/src/layout.ts +325 -5
- package/src/paint.ts +155 -52
- package/src/primitives/stacks.ts +1 -1
- package/src/primitives/text-input.ts +1 -1
- package/src/primitives/text.ts +1 -1
- package/src/terminal.ts +12 -1
- package/src/text-edit.ts +52 -44
package/src/layout.ts
CHANGED
|
@@ -86,20 +86,23 @@ function intrinsicMainSize(
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (node.type === "textinput") {
|
|
89
|
+
const tiPadX = (node.props.padding?.x ?? 0) * 2;
|
|
90
|
+
const tiPadY = (node.props.padding?.y ?? 0) * 2;
|
|
89
91
|
if (isVertical) {
|
|
90
92
|
const val = node.props.value || "";
|
|
91
|
-
|
|
93
|
+
const innerCrossForTI = Math.max(1, crossSize - tiPadX);
|
|
94
|
+
if (val.length === 0) return 1 + tiPadY;
|
|
92
95
|
const lines = val.split("\n");
|
|
93
96
|
let total = 0;
|
|
94
97
|
for (const line of lines) {
|
|
95
98
|
total += Math.max(
|
|
96
99
|
1,
|
|
97
|
-
Math.ceil(visibleWidth(line) / Math.max(1,
|
|
100
|
+
Math.ceil(visibleWidth(line) / Math.max(1, innerCrossForTI)),
|
|
98
101
|
);
|
|
99
102
|
}
|
|
100
|
-
return total;
|
|
103
|
+
return total + tiPadY;
|
|
101
104
|
}
|
|
102
|
-
return 0;
|
|
105
|
+
return 0 + tiPadX;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
// Container: compute intrinsic size along the requested axis.
|
|
@@ -141,6 +144,47 @@ function intrinsicMainSize(
|
|
|
141
144
|
return total + padMain;
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
// Special case: wrapping HStack computing intrinsic height.
|
|
148
|
+
// Instead of max-of-children, simulate the row layout and sum row heights.
|
|
149
|
+
if (
|
|
150
|
+
node.type === "hstack" &&
|
|
151
|
+
(node.props as ContainerProps).flexWrap === "wrap"
|
|
152
|
+
) {
|
|
153
|
+
const wrapWidths: number[] = [];
|
|
154
|
+
const wrapHeights: number[] = [];
|
|
155
|
+
for (const child of node.children) {
|
|
156
|
+
const cProps = getProps(child);
|
|
157
|
+
const flex = cProps?.flex ?? 0;
|
|
158
|
+
let w: number;
|
|
159
|
+
if (flex > 0) {
|
|
160
|
+
w = cProps?.minWidth ?? 0;
|
|
161
|
+
} else {
|
|
162
|
+
w =
|
|
163
|
+
resolveSizeValue(cProps?.width, innerCross) ??
|
|
164
|
+
intrinsicMainSize(child, false, innerCross);
|
|
165
|
+
if (cProps) {
|
|
166
|
+
w = clamp(w, cProps.minWidth ?? 0, cProps.maxWidth ?? Infinity);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
wrapWidths.push(w);
|
|
170
|
+
const h =
|
|
171
|
+
resolveSizeValue(cProps?.height, 0) ??
|
|
172
|
+
intrinsicMainSize(child, true, innerCross);
|
|
173
|
+
wrapHeights.push(h);
|
|
174
|
+
}
|
|
175
|
+
const wrapRows = assignWrapRows(wrapWidths, innerCross, gap);
|
|
176
|
+
let total = 0;
|
|
177
|
+
for (let ri = 0; ri < wrapRows.length; ri++) {
|
|
178
|
+
let maxH = 0;
|
|
179
|
+
for (const idx of wrapRows[ri]!) {
|
|
180
|
+
if (wrapHeights[idx]! > maxH) maxH = wrapHeights[idx]!;
|
|
181
|
+
}
|
|
182
|
+
total += maxH;
|
|
183
|
+
if (ri < wrapRows.length - 1) total += gap;
|
|
184
|
+
}
|
|
185
|
+
return total + padMain;
|
|
186
|
+
}
|
|
187
|
+
|
|
144
188
|
// Cross axis: max of children on the requested axis
|
|
145
189
|
let maxSize = 0;
|
|
146
190
|
for (const child of node.children) {
|
|
@@ -156,11 +200,61 @@ function intrinsicMainSize(
|
|
|
156
200
|
resolveSizeValue(cProps?.width, 0) ??
|
|
157
201
|
intrinsicMainSize(child, false, innerCross);
|
|
158
202
|
}
|
|
203
|
+
// Apply child's cross-axis constraints (e.g. maxHeight on a TextInput
|
|
204
|
+
// inside an HStack) so the container's intrinsic size respects them.
|
|
205
|
+
if (cProps) {
|
|
206
|
+
const minCross = isVertical
|
|
207
|
+
? (cProps.minHeight ?? 0)
|
|
208
|
+
: (cProps.minWidth ?? 0);
|
|
209
|
+
const maxCross = isVertical
|
|
210
|
+
? (cProps.maxHeight ?? Infinity)
|
|
211
|
+
: (cProps.maxWidth ?? Infinity);
|
|
212
|
+
childSize = clamp(childSize, minCross, maxCross);
|
|
213
|
+
}
|
|
159
214
|
if (childSize > maxSize) maxSize = childSize;
|
|
160
215
|
}
|
|
161
216
|
return maxSize + padMain;
|
|
162
217
|
}
|
|
163
218
|
|
|
219
|
+
// --- Wrap row assignment ---
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Assign children to rows for a wrapping HStack.
|
|
223
|
+
* Children are placed left-to-right; when adding the next child (plus gap)
|
|
224
|
+
* would exceed availWidth, a new row begins. A child wider than the
|
|
225
|
+
* container still gets its own row.
|
|
226
|
+
*/
|
|
227
|
+
function assignWrapRows(
|
|
228
|
+
widths: number[],
|
|
229
|
+
availWidth: number,
|
|
230
|
+
gap: number,
|
|
231
|
+
): number[][] {
|
|
232
|
+
if (widths.length === 0) return [];
|
|
233
|
+
const rows: number[][] = [];
|
|
234
|
+
let currentRow: number[] = [];
|
|
235
|
+
let rowWidth = 0;
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < widths.length; i++) {
|
|
238
|
+
const w = widths[i]!;
|
|
239
|
+
if (currentRow.length === 0) {
|
|
240
|
+
currentRow.push(i);
|
|
241
|
+
rowWidth = w;
|
|
242
|
+
} else {
|
|
243
|
+
const needed = rowWidth + gap + w;
|
|
244
|
+
if (needed > availWidth) {
|
|
245
|
+
rows.push(currentRow);
|
|
246
|
+
currentRow = [i];
|
|
247
|
+
rowWidth = w;
|
|
248
|
+
} else {
|
|
249
|
+
currentRow.push(i);
|
|
250
|
+
rowWidth = needed;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (currentRow.length > 0) rows.push(currentRow);
|
|
255
|
+
return rows;
|
|
256
|
+
}
|
|
257
|
+
|
|
164
258
|
// --- Largest remainder rounding ---
|
|
165
259
|
|
|
166
260
|
function largestRemainder(fractions: number[], total: number): number[] {
|
|
@@ -202,6 +296,207 @@ export function layout(
|
|
|
202
296
|
return layoutNode(root, 0, 0, rootW, rootH);
|
|
203
297
|
}
|
|
204
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Layout a wrapping HStack. Children are assigned to rows based on their
|
|
301
|
+
* base widths, then each row is laid out as an independent flex context.
|
|
302
|
+
*/
|
|
303
|
+
function layoutWrapHStack(
|
|
304
|
+
node: Node,
|
|
305
|
+
rect: Rect,
|
|
306
|
+
props: ContainerProps,
|
|
307
|
+
children: Node[],
|
|
308
|
+
x: number,
|
|
309
|
+
y: number,
|
|
310
|
+
width: number,
|
|
311
|
+
height: number,
|
|
312
|
+
): LayoutNode {
|
|
313
|
+
const gap = props.gap ?? 0;
|
|
314
|
+
const align = props.alignItems ?? "stretch";
|
|
315
|
+
const justify = props.justifyContent ?? "start";
|
|
316
|
+
|
|
317
|
+
// Padding
|
|
318
|
+
const padX = props.padding?.x ?? 0;
|
|
319
|
+
const padY = props.padding?.y ?? 0;
|
|
320
|
+
const innerX = x + padX;
|
|
321
|
+
const innerY = y + padY;
|
|
322
|
+
const innerW = Math.max(0, width - padX * 2);
|
|
323
|
+
const innerH = Math.max(0, height - padY * 2);
|
|
324
|
+
|
|
325
|
+
// Phase 1: Measure each child's base width and cross (height) size
|
|
326
|
+
const baseWidths: number[] = [];
|
|
327
|
+
const crossSizes: number[] = [];
|
|
328
|
+
const flexValues: number[] = [];
|
|
329
|
+
|
|
330
|
+
for (const child of children) {
|
|
331
|
+
const cProps = getProps(child);
|
|
332
|
+
const flex = cProps?.flex ?? 0;
|
|
333
|
+
flexValues.push(flex);
|
|
334
|
+
|
|
335
|
+
let baseW: number;
|
|
336
|
+
if (flex > 0) {
|
|
337
|
+
// Flex children use minWidth for row assignment (like CSS flex-basis)
|
|
338
|
+
baseW = cProps?.minWidth ?? 0;
|
|
339
|
+
} else {
|
|
340
|
+
baseW =
|
|
341
|
+
resolveSizeValue(cProps?.width, innerW) ??
|
|
342
|
+
intrinsicMainSize(child, false, innerH);
|
|
343
|
+
if (cProps) {
|
|
344
|
+
baseW = clamp(baseW, cProps.minWidth ?? 0, cProps.maxWidth ?? Infinity);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
baseWidths.push(baseW);
|
|
348
|
+
|
|
349
|
+
// Always compute real cross size (explicit or intrinsic) for row height
|
|
350
|
+
let cross =
|
|
351
|
+
resolveSizeValue(cProps?.height, innerH) ??
|
|
352
|
+
intrinsicMainSize(child, true, innerW);
|
|
353
|
+
// Apply cross-axis constraints (e.g. maxHeight)
|
|
354
|
+
if (cProps) {
|
|
355
|
+
cross = clamp(cross, cProps.minHeight ?? 0, cProps.maxHeight ?? Infinity);
|
|
356
|
+
}
|
|
357
|
+
crossSizes.push(cross);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Phase 2: Assign children to rows
|
|
361
|
+
const rows = assignWrapRows(baseWidths, innerW, gap);
|
|
362
|
+
|
|
363
|
+
// Phase 3: Layout each row independently
|
|
364
|
+
const layoutChildren: LayoutNode[] = [];
|
|
365
|
+
let rowY = 0;
|
|
366
|
+
|
|
367
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
368
|
+
const rowIdx = rows[ri]!;
|
|
369
|
+
const rowGapTotal = gap * (rowIdx.length - 1);
|
|
370
|
+
const rowAvail = innerW - rowGapTotal;
|
|
371
|
+
|
|
372
|
+
// Compute fixed and flex totals for this row
|
|
373
|
+
let fixedMain = 0;
|
|
374
|
+
let totalFlex = 0;
|
|
375
|
+
for (const idx of rowIdx) {
|
|
376
|
+
if (flexValues[idx]! > 0) {
|
|
377
|
+
totalFlex += flexValues[idx]!;
|
|
378
|
+
} else {
|
|
379
|
+
fixedMain += baseWidths[idx]!;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Distribute flex space within this row
|
|
384
|
+
const flexSpace = Math.max(0, rowAvail - fixedMain);
|
|
385
|
+
const childWidths: number[] = new Array(rowIdx.length);
|
|
386
|
+
|
|
387
|
+
if (totalFlex > 0) {
|
|
388
|
+
const flexPositions: number[] = [];
|
|
389
|
+
for (let ci = 0; ci < rowIdx.length; ci++) {
|
|
390
|
+
if (flexValues[rowIdx[ci]!]! > 0) flexPositions.push(ci);
|
|
391
|
+
}
|
|
392
|
+
const rawSizes = flexPositions.map(
|
|
393
|
+
(ci) => (flexValues[rowIdx[ci]!]! / totalFlex) * flexSpace,
|
|
394
|
+
);
|
|
395
|
+
const rounded = largestRemainder(rawSizes, flexSpace);
|
|
396
|
+
|
|
397
|
+
for (let fi = 0; fi < flexPositions.length; fi++) {
|
|
398
|
+
const ci = flexPositions[fi]!;
|
|
399
|
+
let size = rounded[fi]!;
|
|
400
|
+
const cProps = getProps(children[rowIdx[ci]!]!);
|
|
401
|
+
if (cProps) {
|
|
402
|
+
size = clamp(size, cProps.minWidth ?? 0, cProps.maxWidth ?? Infinity);
|
|
403
|
+
}
|
|
404
|
+
childWidths[ci] = size;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Non-flex children keep their base width
|
|
409
|
+
for (let ci = 0; ci < rowIdx.length; ci++) {
|
|
410
|
+
if (childWidths[ci] === undefined) {
|
|
411
|
+
childWidths[ci] = baseWidths[rowIdx[ci]!]!;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Row height = max cross size of children in this row
|
|
416
|
+
let rowHeight = 0;
|
|
417
|
+
for (const idx of rowIdx) {
|
|
418
|
+
if (crossSizes[idx]! > rowHeight) rowHeight = crossSizes[idx]!;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// justifyContent: compute main-axis starting offset
|
|
422
|
+
let totalUsedWidth = rowGapTotal;
|
|
423
|
+
for (let ci = 0; ci < rowIdx.length; ci++) {
|
|
424
|
+
totalUsedWidth += childWidths[ci]!;
|
|
425
|
+
}
|
|
426
|
+
const remainingMain = Math.max(0, innerW - totalUsedWidth);
|
|
427
|
+
|
|
428
|
+
let mainStart = 0;
|
|
429
|
+
let betweenGaps: number[] | null = null;
|
|
430
|
+
|
|
431
|
+
if (justify === "end") {
|
|
432
|
+
mainStart = remainingMain;
|
|
433
|
+
} else if (justify === "center") {
|
|
434
|
+
mainStart = Math.floor(remainingMain / 2);
|
|
435
|
+
} else if (justify === "space-between" && rowIdx.length > 1) {
|
|
436
|
+
const gapCount = rowIdx.length - 1;
|
|
437
|
+
const rawGaps = Array.from(
|
|
438
|
+
{ length: gapCount },
|
|
439
|
+
() => remainingMain / gapCount,
|
|
440
|
+
);
|
|
441
|
+
betweenGaps = largestRemainder(rawGaps, remainingMain);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Position children in this row
|
|
445
|
+
let xOffset = mainStart;
|
|
446
|
+
for (let ci = 0; ci < rowIdx.length; ci++) {
|
|
447
|
+
const idx = rowIdx[ci]!;
|
|
448
|
+
const childW = childWidths[ci]!;
|
|
449
|
+
|
|
450
|
+
// Cross-axis sizing: stretch fills row height, others keep their size
|
|
451
|
+
let childH: number;
|
|
452
|
+
if (align === "stretch") {
|
|
453
|
+
const cProps = getProps(children[idx]!);
|
|
454
|
+
const explicitH = resolveSizeValue(cProps?.height, innerH);
|
|
455
|
+
childH = explicitH ?? rowHeight;
|
|
456
|
+
} else {
|
|
457
|
+
childH = crossSizes[idx]!;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Cross-axis alignment within the row
|
|
461
|
+
let crossOffset = 0;
|
|
462
|
+
if (align === "center") {
|
|
463
|
+
crossOffset = Math.floor((rowHeight - childH) / 2);
|
|
464
|
+
} else if (align === "end") {
|
|
465
|
+
crossOffset = rowHeight - childH;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const childX = innerX + xOffset;
|
|
469
|
+
const childY = innerY + rowY + crossOffset;
|
|
470
|
+
|
|
471
|
+
layoutChildren.push(
|
|
472
|
+
layoutNode(children[idx]!, childX, childY, childW, childH),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
xOffset += childW;
|
|
476
|
+
if (ci < rowIdx.length - 1) {
|
|
477
|
+
xOffset += gap;
|
|
478
|
+
if (betweenGaps) {
|
|
479
|
+
xOffset += betweenGaps[ci]!;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
rowY += rowHeight;
|
|
485
|
+
if (ri < rows.length - 1) {
|
|
486
|
+
rowY += gap;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Intrinsic height: if no explicit height, shrink to fit all rows
|
|
491
|
+
const hasExplicitHeight =
|
|
492
|
+
props.height !== undefined || props.flex !== undefined;
|
|
493
|
+
if (!hasExplicitHeight) {
|
|
494
|
+
rect.height = rowY + padY * 2;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return { node, rect, children: layoutChildren };
|
|
498
|
+
}
|
|
499
|
+
|
|
205
500
|
/**
|
|
206
501
|
* Layout a node within the given available space.
|
|
207
502
|
* Resolves the node's own explicit size (if any) against available space,
|
|
@@ -236,6 +531,11 @@ function layoutNode(
|
|
|
236
531
|
return { node, rect, children: [] };
|
|
237
532
|
}
|
|
238
533
|
|
|
534
|
+
// Wrapping HStack: separate layout path
|
|
535
|
+
if (node.type === "hstack" && props.flexWrap === "wrap") {
|
|
536
|
+
return layoutWrapHStack(node, rect, props, children, x, y, width, height);
|
|
537
|
+
}
|
|
538
|
+
|
|
239
539
|
// Padding
|
|
240
540
|
const padX = props.padding?.x ?? 0;
|
|
241
541
|
const padY = props.padding?.y ?? 0;
|
|
@@ -281,6 +581,16 @@ function layoutNode(
|
|
|
281
581
|
resolveSizeValue(cProps?.height, innerH) ??
|
|
282
582
|
(useIntrinsicCross ? intrinsicMainSize(child, true, innerW) : innerH);
|
|
283
583
|
}
|
|
584
|
+
// Apply cross-axis constraints
|
|
585
|
+
if (cProps) {
|
|
586
|
+
const minCross = isVertical
|
|
587
|
+
? (cProps.minWidth ?? 0)
|
|
588
|
+
: (cProps.minHeight ?? 0);
|
|
589
|
+
const maxCross = isVertical
|
|
590
|
+
? (cProps.maxWidth ?? Infinity)
|
|
591
|
+
: (cProps.maxHeight ?? Infinity);
|
|
592
|
+
cross = clamp(cross, minCross, maxCross);
|
|
593
|
+
}
|
|
284
594
|
infos.push({ node: child, mainSize: 0, crossSize: cross, flex });
|
|
285
595
|
} else {
|
|
286
596
|
// Main-axis: explicit → percentage → intrinsic
|
|
@@ -304,7 +614,7 @@ function layoutNode(
|
|
|
304
614
|
(useIntrinsicCross ? intrinsicMainSize(child, true, innerW) : innerH);
|
|
305
615
|
}
|
|
306
616
|
|
|
307
|
-
// Apply constraints
|
|
617
|
+
// Apply main-axis constraints
|
|
308
618
|
if (cProps) {
|
|
309
619
|
const minMain = isVertical
|
|
310
620
|
? (cProps.minHeight ?? 0)
|
|
@@ -314,6 +624,16 @@ function layoutNode(
|
|
|
314
624
|
: (cProps.maxWidth ?? Infinity);
|
|
315
625
|
main = clamp(main, minMain, maxMain);
|
|
316
626
|
}
|
|
627
|
+
// Apply cross-axis constraints
|
|
628
|
+
if (cProps) {
|
|
629
|
+
const minCross = isVertical
|
|
630
|
+
? (cProps.minWidth ?? 0)
|
|
631
|
+
: (cProps.minHeight ?? 0);
|
|
632
|
+
const maxCross = isVertical
|
|
633
|
+
? (cProps.maxWidth ?? Infinity)
|
|
634
|
+
: (cProps.maxHeight ?? Infinity);
|
|
635
|
+
cross = clamp(cross, minCross, maxCross);
|
|
636
|
+
}
|
|
317
637
|
|
|
318
638
|
fixedMain += main;
|
|
319
639
|
infos.push({ node: child, mainSize: main, crossSize: cross, flex });
|