@uniflowed/tui 0.0.0-alpha.18 → 0.14.1
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/capability.js +15 -12
- package/cells.js +1 -1
- package/clipboard.js +61 -0
- package/components.js +766 -57
- package/index.js +41 -22
- package/internal/hits.js +2 -2
- package/internal/host.js +63 -22
- package/internal/paint.js +43 -2
- package/internal/tree.js +168 -38
- package/internal/widgets.js +771 -0
- package/keys.js +214 -15
- package/layout.js +653 -132
- package/package.json +5 -4
- package/terminal.js +49 -19
- package/widths.js +4 -1
package/layout.js
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
//
|
|
25
25
|
// So this implements the subset a terminal uses, in whole cells. What it does
|
|
26
26
|
// *not* implement is written down at the bottom of this file rather than
|
|
27
|
-
// discovered: no
|
|
28
|
-
//
|
|
27
|
+
// discovered: no aspect ratio, and no wrapping or positioning inside a
|
|
28
|
+
// scrolling box. Those are ubugeeei-prod/uf#314.
|
|
29
29
|
//
|
|
30
30
|
// # Whole cells, and where the remainder goes
|
|
31
31
|
//
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
*/
|
|
46
46
|
export type Dimension = number | string;
|
|
47
47
|
|
|
48
|
+
/** A margin: a number of cells, or `"auto"` inside a flex container. */
|
|
49
|
+
export type Margin = number | "auto";
|
|
50
|
+
|
|
48
51
|
/** Main-axis direction. `"column"` is the default, as in OpenTUI. */
|
|
49
52
|
export type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse";
|
|
50
53
|
|
|
@@ -63,6 +66,45 @@ export type AlignItems = "flex-start" | "center" | "flex-end" | "stretch";
|
|
|
63
66
|
/** Cross-axis alignment of one child, or `"auto"` to follow the parent. */
|
|
64
67
|
export type AlignSelf = "auto" | AlignItems;
|
|
65
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Whether children that do not fit on one line go on to another.
|
|
71
|
+
*
|
|
72
|
+
* `"no-wrap"` — the default, OpenTUI's and Yoga's — keeps them on one and
|
|
73
|
+
* shrinks them. `"wrap"` starts a new line below (or, in a column, to the
|
|
74
|
+
* right), and `"wrap-reverse"` stacks the lines from the other side.
|
|
75
|
+
*/
|
|
76
|
+
export type FlexWrap = "no-wrap" | "wrap" | "wrap-reverse";
|
|
77
|
+
|
|
78
|
+
/** Where the lines of a wrapping box go in the space they leave over. */
|
|
79
|
+
export type AlignContent =
|
|
80
|
+
| "flex-start"
|
|
81
|
+
| "center"
|
|
82
|
+
| "flex-end"
|
|
83
|
+
| "stretch"
|
|
84
|
+
| "space-between"
|
|
85
|
+
| "space-around"
|
|
86
|
+
| "space-evenly";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a node takes part in its parent's flex line, and whether it is a
|
|
90
|
+
* containing block for the absolutely positioned boxes under it.
|
|
91
|
+
*
|
|
92
|
+
* `"relative"` — the default, as it is in OpenTUI and Yoga — takes part, and
|
|
93
|
+
* its `top`/`right`/`bottom`/`left` then nudge where it is drawn without
|
|
94
|
+
* moving anything around it. `"absolute"` does not: its parent lays out as if
|
|
95
|
+
* it were not there, and it is placed by those four offsets against the inside
|
|
96
|
+
* of its containing block's border. Both are *positioned*, which is what makes
|
|
97
|
+
* a box a containing block.
|
|
98
|
+
*
|
|
99
|
+
* `"static"` takes part in the line like `"relative"`, ignores the four
|
|
100
|
+
* offsets, and is not a containing block: an absolutely positioned box under
|
|
101
|
+
* it looks past it to the nearest ancestor that is positioned, or to the root.
|
|
102
|
+
* That is CSS's rule and Yoga 3's, and it is the only way for a box to be
|
|
103
|
+
* placed against something other than its parent — which is what a tooltip
|
|
104
|
+
* inside a row of a panel needs to sit against the panel.
|
|
105
|
+
*/
|
|
106
|
+
export type Position = "static" | "relative" | "absolute";
|
|
107
|
+
|
|
66
108
|
/**
|
|
67
109
|
* What happens to content larger than its box.
|
|
68
110
|
*
|
|
@@ -83,8 +125,10 @@ export type Overflow = "visible" | "hidden" | "scroll";
|
|
|
83
125
|
*/
|
|
84
126
|
export type LayoutStyle = {
|
|
85
127
|
readonly flexDirection?: FlexDirection,
|
|
128
|
+
readonly flexWrap?: FlexWrap,
|
|
86
129
|
readonly justifyContent?: JustifyContent,
|
|
87
130
|
readonly alignItems?: AlignItems,
|
|
131
|
+
readonly alignContent?: AlignContent,
|
|
88
132
|
readonly alignSelf?: AlignSelf,
|
|
89
133
|
readonly flexGrow?: number,
|
|
90
134
|
readonly flexShrink?: number,
|
|
@@ -100,15 +144,21 @@ export type LayoutStyle = {
|
|
|
100
144
|
readonly paddingRight?: number,
|
|
101
145
|
readonly paddingBottom?: number,
|
|
102
146
|
readonly paddingLeft?: number,
|
|
103
|
-
readonly margin?:
|
|
104
|
-
readonly marginTop?:
|
|
105
|
-
readonly marginRight?:
|
|
106
|
-
readonly marginBottom?:
|
|
107
|
-
readonly marginLeft?:
|
|
147
|
+
readonly margin?: Margin,
|
|
148
|
+
readonly marginTop?: Margin,
|
|
149
|
+
readonly marginRight?: Margin,
|
|
150
|
+
readonly marginBottom?: Margin,
|
|
151
|
+
readonly marginLeft?: Margin,
|
|
108
152
|
readonly gap?: number,
|
|
109
153
|
readonly rowGap?: number,
|
|
110
154
|
readonly columnGap?: number,
|
|
111
155
|
readonly overflow?: Overflow,
|
|
156
|
+
readonly position?: Position,
|
|
157
|
+
/** Offsets: cells, or a percentage of the parent's inside. Negative is allowed. */
|
|
158
|
+
readonly top?: Dimension,
|
|
159
|
+
readonly right?: Dimension,
|
|
160
|
+
readonly bottom?: Dimension,
|
|
161
|
+
readonly left?: Dimension,
|
|
112
162
|
/**
|
|
113
163
|
* The first content row a scrolling box shows.
|
|
114
164
|
*
|
|
@@ -132,7 +182,12 @@ export type LayoutStyle = {
|
|
|
132
182
|
*/
|
|
133
183
|
export type LayoutNode = {
|
|
134
184
|
style: LayoutStyle,
|
|
135
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Read-only here because layout never adds or removes a child. That is also
|
|
187
|
+
* what lets the renderer's `TuiNode`, whose children are `TuiNode`s, be laid
|
|
188
|
+
* out as a `LayoutNode`: a mutable array would be invariant in its element.
|
|
189
|
+
*/
|
|
190
|
+
readonly children: $ReadOnlyArray<LayoutNode>,
|
|
136
191
|
/** Cells the node's own frame occupies on each edge; a border is 1. */
|
|
137
192
|
borderWidth: number,
|
|
138
193
|
measure: ((availableWidth: number, availableHeight: number) => Size) | null,
|
|
@@ -235,6 +290,8 @@ export type Size = { readonly width: number, readonly height: number };
|
|
|
235
290
|
const clamp = (value: number, low: number, high: number): number =>
|
|
236
291
|
Math.min(Math.max(value, low), high);
|
|
237
292
|
|
|
293
|
+
type Edges<T> = [T, T, T, T];
|
|
294
|
+
|
|
238
295
|
/** Whether the main axis is horizontal. */
|
|
239
296
|
const isRow = (direction: FlexDirection): boolean =>
|
|
240
297
|
direction === "row" || direction === "row-reverse";
|
|
@@ -260,8 +317,73 @@ function resolve(value: Dimension | void, basis: number): number | null {
|
|
|
260
317
|
return null;
|
|
261
318
|
}
|
|
262
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Resolve an offset, which unlike a length may be negative.
|
|
322
|
+
*
|
|
323
|
+
* `top: -1` on a badge is how it sits on its parent's border, so the clamp
|
|
324
|
+
* {@link resolve} applies to a size would move it back inside.
|
|
325
|
+
*/
|
|
326
|
+
function resolveOffset(value: Dimension | void, basis: number): number | null {
|
|
327
|
+
if (typeof value === "number") {
|
|
328
|
+
return Number.isFinite(value) ? Math.round(value) : null;
|
|
329
|
+
}
|
|
330
|
+
if (typeof value === "string" && value.endsWith("%")) {
|
|
331
|
+
const percent = Number.parseFloat(value.slice(0, -1));
|
|
332
|
+
return Number.isFinite(percent) ? Math.round((basis * percent) / 100) : null;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Whether a child is out of its parent's flex line. */
|
|
338
|
+
const isAbsolute = (node: LayoutNode): boolean => node.style.position === "absolute";
|
|
339
|
+
|
|
340
|
+
/** Whether a node is a containing block for the absolute boxes under it. */
|
|
341
|
+
const isPositioned = (node: LayoutNode): boolean => node.style.position !== "static";
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The box an absolutely positioned descendant is placed against: the padding
|
|
345
|
+
* box of its nearest positioned ancestor, in frame coordinates.
|
|
346
|
+
*/
|
|
347
|
+
type ContainingBlock = {
|
|
348
|
+
readonly x: number,
|
|
349
|
+
readonly y: number,
|
|
350
|
+
readonly width: number,
|
|
351
|
+
readonly height: number,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/** A node's padding box — inside its border, not inside its padding. */
|
|
355
|
+
function paddingBox(node: LayoutNode): ContainingBlock {
|
|
356
|
+
const border = node.borderWidth;
|
|
357
|
+
return {
|
|
358
|
+
x: node.x + border,
|
|
359
|
+
y: node.y + border,
|
|
360
|
+
width: Math.max(0, node.width - border * 2),
|
|
361
|
+
height: Math.max(0, node.height - border * 2),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* How far a relatively positioned child is drawn from where the line put it.
|
|
367
|
+
*
|
|
368
|
+
* `left` wins over `right` and `top` over `bottom` when both are given, which
|
|
369
|
+
* is CSS's rule for a box whose width is already decided.
|
|
370
|
+
*/
|
|
371
|
+
function relativeShift(style: LayoutStyle, width: number, height: number): [number, number] {
|
|
372
|
+
// A static box is exactly where its line put it. The offsets are not an
|
|
373
|
+
// error on one, only unread — which is what lets a caller flip `position`
|
|
374
|
+
// without also deleting the props it had.
|
|
375
|
+
if (style.position === "static") {
|
|
376
|
+
return [0, 0];
|
|
377
|
+
}
|
|
378
|
+
const left = resolveOffset(style.left, width);
|
|
379
|
+
const right = resolveOffset(style.right, width);
|
|
380
|
+
const top = resolveOffset(style.top, height);
|
|
381
|
+
const bottom = resolveOffset(style.bottom, height);
|
|
382
|
+
return [left ?? (right != null ? -right : 0), top ?? (bottom != null ? -bottom : 0)];
|
|
383
|
+
}
|
|
384
|
+
|
|
263
385
|
/** Padding on each edge, with the shorthand applied first. */
|
|
264
|
-
function padding(style: LayoutStyle):
|
|
386
|
+
function padding(style: LayoutStyle): Edges<number> {
|
|
265
387
|
const all = style.padding ?? 0;
|
|
266
388
|
return [
|
|
267
389
|
style.paddingTop ?? all,
|
|
@@ -272,7 +394,7 @@ function padding(style: LayoutStyle): [number, number, number, number] {
|
|
|
272
394
|
}
|
|
273
395
|
|
|
274
396
|
/** Margin on each edge, with the shorthand applied first. */
|
|
275
|
-
function margin(style: LayoutStyle):
|
|
397
|
+
function margin(style: LayoutStyle): Edges<Margin> {
|
|
276
398
|
const all = style.margin ?? 0;
|
|
277
399
|
return [
|
|
278
400
|
style.marginTop ?? all,
|
|
@@ -282,6 +404,21 @@ function margin(style: LayoutStyle): [number, number, number, number] {
|
|
|
282
404
|
];
|
|
283
405
|
}
|
|
284
406
|
|
|
407
|
+
/** The fixed part of a margin. `auto` is resolved only by the flex parent. */
|
|
408
|
+
function marginCells(value: Margin): number {
|
|
409
|
+
return value === "auto" ? 0 : value;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** A margin tuple with every `auto` edge treated as zero cells. */
|
|
413
|
+
function fixedMargins(edges: Edges<Margin>): Edges<number> {
|
|
414
|
+
return [
|
|
415
|
+
marginCells(edges[0]),
|
|
416
|
+
marginCells(edges[1]),
|
|
417
|
+
marginCells(edges[2]),
|
|
418
|
+
marginCells(edges[3]),
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
|
|
285
422
|
/** The cells between two children along the main axis. */
|
|
286
423
|
function gapOf(style: LayoutStyle, row: boolean): number {
|
|
287
424
|
const specific = row ? style.columnGap : style.rowGap;
|
|
@@ -384,6 +521,16 @@ function measureIntrinsic(node: LayoutNode, availableWidth: number, availableHei
|
|
|
384
521
|
fixedHeight != null
|
|
385
522
|
? 0
|
|
386
523
|
: scrollStack(node, innerAvailableWidth, innerAvailableHeight).content;
|
|
524
|
+
} else if (wraps(style)) {
|
|
525
|
+
const size = wrappedSize(
|
|
526
|
+
node,
|
|
527
|
+
fixedWidth != null ? Math.max(0, fixedWidth - insetLeft - insetRight) : innerAvailableWidth,
|
|
528
|
+
fixedHeight != null
|
|
529
|
+
? Math.max(0, fixedHeight - insetTop - insetBottom)
|
|
530
|
+
: innerAvailableHeight,
|
|
531
|
+
);
|
|
532
|
+
contentWidth = size.width;
|
|
533
|
+
contentHeight = size.height;
|
|
387
534
|
} else {
|
|
388
535
|
const row = isRow(direction(node.style));
|
|
389
536
|
const gap = gapOf(style, row);
|
|
@@ -391,7 +538,12 @@ function measureIntrinsic(node: LayoutNode, availableWidth: number, availableHei
|
|
|
391
538
|
let cross = 0;
|
|
392
539
|
let counted = 0;
|
|
393
540
|
for (const child of node.children) {
|
|
394
|
-
|
|
541
|
+
// A child out of the line takes no room in it, so it cannot make its
|
|
542
|
+
// parent any larger — which is the whole of what `absolute` means.
|
|
543
|
+
if (isAbsolute(child)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = fixedMargins(margin(child.style));
|
|
395
547
|
const size = intrinsicSize(
|
|
396
548
|
child,
|
|
397
549
|
Math.max(0, innerAvailableWidth - marginLeft - marginRight),
|
|
@@ -416,6 +568,55 @@ function measureIntrinsic(node: LayoutNode, availableWidth: number, availableHei
|
|
|
416
568
|
};
|
|
417
569
|
}
|
|
418
570
|
|
|
571
|
+
/**
|
|
572
|
+
* The content size of a box that wraps, in the space it is offered.
|
|
573
|
+
*
|
|
574
|
+
* The lines it would break into there, each as long as its children and as
|
|
575
|
+
* deep as its deepest, stacked with the gap between lines. Which is why a
|
|
576
|
+
* wrapping row offered forty columns can come back narrower than forty and
|
|
577
|
+
* taller than one line: it is as wide as its widest line.
|
|
578
|
+
*/
|
|
579
|
+
function wrappedSize(node: LayoutNode, width: number, height: number): Size {
|
|
580
|
+
const row = isRow(direction(node.style));
|
|
581
|
+
const gap = gapOf(node.style, row);
|
|
582
|
+
const outer: Array<{ main: number, cross: number }> = [];
|
|
583
|
+
for (const child of node.children) {
|
|
584
|
+
if (isAbsolute(child)) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = fixedMargins(margin(child.style));
|
|
588
|
+
const size = intrinsicSize(
|
|
589
|
+
child,
|
|
590
|
+
Math.max(0, width - marginLeft - marginRight),
|
|
591
|
+
Math.max(0, height - marginTop - marginBottom),
|
|
592
|
+
);
|
|
593
|
+
const outerWidth = size.width + marginLeft + marginRight;
|
|
594
|
+
const outerHeight = size.height + marginTop + marginBottom;
|
|
595
|
+
outer.push(
|
|
596
|
+
row ? { main: outerWidth, cross: outerHeight } : { main: outerHeight, cross: outerWidth },
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const lines = breakLines(
|
|
600
|
+
outer.map((each) => each.main),
|
|
601
|
+
row ? width : height,
|
|
602
|
+
gap,
|
|
603
|
+
);
|
|
604
|
+
let main = 0;
|
|
605
|
+
let cross = 0;
|
|
606
|
+
for (const line of lines) {
|
|
607
|
+
let length = Math.max(0, line.length - 1) * gap;
|
|
608
|
+
let depth = 0;
|
|
609
|
+
for (const index of line) {
|
|
610
|
+
length += outer[index].main;
|
|
611
|
+
depth = Math.max(depth, outer[index].cross);
|
|
612
|
+
}
|
|
613
|
+
main = Math.max(main, length);
|
|
614
|
+
cross += depth;
|
|
615
|
+
}
|
|
616
|
+
cross += Math.max(0, lines.length - 1) * gapOf(node.style, !row);
|
|
617
|
+
return row ? { width: main, height: cross } : { width: cross, height: main };
|
|
618
|
+
}
|
|
619
|
+
|
|
419
620
|
/** Apply `min*`/`max*` to a resolved length. */
|
|
420
621
|
function clampDimension(
|
|
421
622
|
value: number,
|
|
@@ -437,7 +638,7 @@ function clampDimension(
|
|
|
437
638
|
*/
|
|
438
639
|
function distribute(total: number, weights: $ReadOnlyArray<number>): Array<number> {
|
|
439
640
|
const sum = weights.reduce((a, b) => a + b, 0);
|
|
440
|
-
const out = new Array(weights.length).fill(0);
|
|
641
|
+
const out = new Array<number>(weights.length).fill(0);
|
|
441
642
|
if (sum <= 0 || total === 0) {
|
|
442
643
|
return out;
|
|
443
644
|
}
|
|
@@ -452,11 +653,88 @@ function distribute(total: number, weights: $ReadOnlyArray<number>): Array<numbe
|
|
|
452
653
|
return out;
|
|
453
654
|
}
|
|
454
655
|
|
|
656
|
+
/** Whether a box breaks its children onto more than one line. */
|
|
657
|
+
const wraps = (style: LayoutStyle): boolean =>
|
|
658
|
+
style.flexWrap === "wrap" || style.flexWrap === "wrap-reverse";
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Break children, given as their outer main sizes, into lines of `space`.
|
|
662
|
+
*
|
|
663
|
+
* Greedy, which is what flexbox specifies: a line takes children until the
|
|
664
|
+
* next would overflow it. A child larger than a whole line still starts one,
|
|
665
|
+
* so no child is ever left without a line to be on.
|
|
666
|
+
*/
|
|
667
|
+
function breakLines(
|
|
668
|
+
sizes: $ReadOnlyArray<number>,
|
|
669
|
+
space: number,
|
|
670
|
+
gap: number,
|
|
671
|
+
): Array<Array<number>> {
|
|
672
|
+
const lines: Array<Array<number>> = [];
|
|
673
|
+
let line: Array<number> = [];
|
|
674
|
+
let used = 0;
|
|
675
|
+
for (let index = 0; index < sizes.length; index += 1) {
|
|
676
|
+
const size = sizes[index];
|
|
677
|
+
if (line.length > 0 && used + gap + size > space) {
|
|
678
|
+
lines.push(line);
|
|
679
|
+
line = [];
|
|
680
|
+
used = 0;
|
|
681
|
+
}
|
|
682
|
+
used += (line.length > 0 ? gap : 0) + size;
|
|
683
|
+
line.push(index);
|
|
684
|
+
}
|
|
685
|
+
if (line.length > 0) {
|
|
686
|
+
lines.push(line);
|
|
687
|
+
}
|
|
688
|
+
return lines;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Where `alignContent` puts the lines of a wrapping box, given the cross-axis
|
|
693
|
+
* space they leave: the first line's offset, the extra space between two
|
|
694
|
+
* lines, and the space `"stretch"` hands out to the lines themselves.
|
|
695
|
+
*
|
|
696
|
+
* `"flex-start"` is the default, which is Yoga's and so OpenTUI's; CSS's is
|
|
697
|
+
* `"stretch"`, and a box copied from a stylesheet with no `alignContent` will
|
|
698
|
+
* pack its lines at the top here where a browser would spread them.
|
|
699
|
+
*/
|
|
700
|
+
function alignLines(
|
|
701
|
+
alignment: AlignContent | void,
|
|
702
|
+
spare: number,
|
|
703
|
+
count: number,
|
|
704
|
+
): [number, number, number] {
|
|
705
|
+
const free = Math.max(0, spare);
|
|
706
|
+
switch (alignment) {
|
|
707
|
+
case "center":
|
|
708
|
+
return [Math.floor(free / 2), 0, 0];
|
|
709
|
+
case "flex-end":
|
|
710
|
+
return [free, 0, 0];
|
|
711
|
+
case "stretch":
|
|
712
|
+
return [0, 0, free];
|
|
713
|
+
case "space-between":
|
|
714
|
+
return count > 1 ? [0, Math.floor(free / (count - 1)), 0] : [0, 0, 0];
|
|
715
|
+
case "space-around": {
|
|
716
|
+
const each = count > 0 ? Math.floor(free / count) : 0;
|
|
717
|
+
return [Math.floor(each / 2), each, 0];
|
|
718
|
+
}
|
|
719
|
+
case "space-evenly": {
|
|
720
|
+
const each = Math.floor(free / (count + 1));
|
|
721
|
+
return [each, each, 0];
|
|
722
|
+
}
|
|
723
|
+
default:
|
|
724
|
+
return [0, 0, 0];
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
455
728
|
/**
|
|
456
729
|
* Lay `node` out into the border box at `x`, `y`, `width` by `height`.
|
|
457
730
|
*
|
|
458
731
|
* Writes `x`, `y`, `width` and `height` onto every node in the subtree. The
|
|
459
732
|
* caller decides the root's box, which for a terminal is the whole screen.
|
|
733
|
+
*
|
|
734
|
+
* `containing` is the containing block the nearest positioned ancestor offers,
|
|
735
|
+
* and only this module passes it: a caller laying out a root has no ancestor,
|
|
736
|
+
* and a root that is `"static"` is then its own containing block — the screen,
|
|
737
|
+
* which is where a box with no positioned ancestor is placed in CSS as well.
|
|
460
738
|
*/
|
|
461
739
|
export function layout(
|
|
462
740
|
node: LayoutNode,
|
|
@@ -464,6 +742,7 @@ export function layout(
|
|
|
464
742
|
y: number,
|
|
465
743
|
width: number,
|
|
466
744
|
height: number,
|
|
745
|
+
containing: ContainingBlock | null = null,
|
|
467
746
|
): void {
|
|
468
747
|
node.x = x;
|
|
469
748
|
node.y = y;
|
|
@@ -485,6 +764,10 @@ export function layout(
|
|
|
485
764
|
}
|
|
486
765
|
|
|
487
766
|
const style = node.style;
|
|
767
|
+
// What an absolute box anywhere under this one is placed against, until a
|
|
768
|
+
// positioned descendant offers another. Decided here, after this node's own
|
|
769
|
+
// box is, because a containing block is a box and not a promise of one.
|
|
770
|
+
const block = isPositioned(node) || containing == null ? paddingBox(node) : containing;
|
|
488
771
|
const [insetTop, insetRight, insetBottom, insetLeft] = insets(node);
|
|
489
772
|
const contentX = x + insetLeft;
|
|
490
773
|
const contentY = y + insetTop;
|
|
@@ -492,7 +775,7 @@ export function layout(
|
|
|
492
775
|
const contentHeight = Math.max(0, height - insetTop - insetBottom);
|
|
493
776
|
|
|
494
777
|
if (style.overflow === "scroll") {
|
|
495
|
-
layoutScroll(node, contentX, contentY, contentWidth, contentHeight);
|
|
778
|
+
layoutScroll(node, contentX, contentY, contentWidth, contentHeight, block);
|
|
496
779
|
return;
|
|
497
780
|
}
|
|
498
781
|
|
|
@@ -502,141 +785,360 @@ export function layout(
|
|
|
502
785
|
const mainSpace = row ? contentWidth : contentHeight;
|
|
503
786
|
const crossSpace = row ? contentHeight : contentWidth;
|
|
504
787
|
const gap = gapOf(style, row);
|
|
505
|
-
|
|
788
|
+
// The flex line is the children that are in it. The filter is skipped when
|
|
789
|
+
// there is nothing to filter, which is every box that has no absolutely
|
|
790
|
+
// positioned child: a list rebuilt per frame per box would be the cost of a
|
|
791
|
+
// feature most trees do not use.
|
|
792
|
+
const children = node.children.some(isAbsolute)
|
|
793
|
+
? node.children.filter((child) => !isAbsolute(child))
|
|
794
|
+
: node.children;
|
|
506
795
|
|
|
507
796
|
// Pass one: every child's base main size, and the outer margins around it.
|
|
508
797
|
const margins = children.map((child) => margin(child.style));
|
|
798
|
+
const resolvedMargins = margins.map((edges) => fixedMargins(edges));
|
|
509
799
|
const bases = children.map((child, index) => {
|
|
510
|
-
const [marginTop, marginRight, marginBottom, marginLeft] =
|
|
800
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = resolvedMargins[index];
|
|
511
801
|
const availableWidth = Math.max(0, contentWidth - marginLeft - marginRight);
|
|
512
802
|
const availableHeight = Math.max(0, contentHeight - marginTop - marginBottom);
|
|
513
803
|
const basis = resolve(child.style.flexBasis, mainSpace);
|
|
514
804
|
if (basis != null) {
|
|
515
805
|
return basis;
|
|
516
806
|
}
|
|
517
|
-
const
|
|
518
|
-
if (
|
|
519
|
-
return
|
|
807
|
+
const fixedMain = resolve(row ? child.style.width : child.style.height, mainSpace);
|
|
808
|
+
if (fixedMain != null) {
|
|
809
|
+
return fixedMain;
|
|
520
810
|
}
|
|
521
811
|
const size = intrinsicSize(child, availableWidth, availableHeight);
|
|
522
812
|
return row ? size.width : size.height;
|
|
523
813
|
});
|
|
524
814
|
|
|
525
815
|
const outerMain = (index: number): number => {
|
|
526
|
-
const [marginTop, marginRight, marginBottom, marginLeft] =
|
|
816
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = resolvedMargins[index];
|
|
527
817
|
return bases[index] + (row ? marginLeft + marginRight : marginTop + marginBottom);
|
|
528
818
|
};
|
|
529
819
|
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
820
|
+
const justify = style.justifyContent ?? "flex-start";
|
|
821
|
+
const parentAlign = style.alignItems ?? "stretch";
|
|
822
|
+
|
|
823
|
+
// Lay one flex line out: grow or shrink it into the main axis, distribute
|
|
824
|
+
// what is left, and align each child within the line's share of the cross
|
|
825
|
+
// axis, which starts `crossOrigin` cells in and is `lineCross` deep. A box
|
|
826
|
+
// that does not wrap has one line, all of it.
|
|
827
|
+
const placeLine = (line: $ReadOnlyArray<number>, crossOrigin: number, lineCross: number) => {
|
|
828
|
+
const used =
|
|
829
|
+
line.reduce((total, index) => total + outerMain(index), 0) +
|
|
830
|
+
Math.max(0, line.length - 1) * gap;
|
|
831
|
+
const free = mainSpace - used;
|
|
832
|
+
|
|
833
|
+
// Pass two: grow into the space left over, or shrink to fit into what there
|
|
834
|
+
// is. Shrinking is weighted by the base size as CSS specifies, so a wide
|
|
835
|
+
// child gives up more columns than a narrow one with the same `flexShrink`.
|
|
836
|
+
const mainSizes = bases.slice();
|
|
837
|
+
if (free > 0) {
|
|
838
|
+
const grow = line.map((index) => Math.max(0, children[index].style.flexGrow ?? 0));
|
|
839
|
+
const shares = distribute(free, grow);
|
|
840
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
841
|
+
mainSizes[line[i]] += shares[i];
|
|
842
|
+
}
|
|
843
|
+
} else if (free < 0) {
|
|
844
|
+
const weights = line.map((index) => shrinkOf(children[index].style, row) * bases[index]);
|
|
845
|
+
const shares = distribute(-free, weights);
|
|
846
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
847
|
+
mainSizes[line[i]] = Math.max(0, mainSizes[line[i]] - shares[i]);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Whatever main-axis space the children did not take, `justifyContent`
|
|
852
|
+
// decides what to do with.
|
|
853
|
+
const consumed =
|
|
854
|
+
line.reduce(
|
|
855
|
+
(total, index) =>
|
|
856
|
+
total +
|
|
857
|
+
mainSizes[index] +
|
|
858
|
+
(row
|
|
859
|
+
? resolvedMargins[index][3] + resolvedMargins[index][1]
|
|
860
|
+
: resolvedMargins[index][0] + resolvedMargins[index][2]),
|
|
861
|
+
0,
|
|
862
|
+
) +
|
|
863
|
+
Math.max(0, line.length - 1) * gap;
|
|
864
|
+
const slack = Math.max(0, mainSpace - consumed);
|
|
865
|
+
|
|
866
|
+
const placed = resolvedMargins.map((edges) => edges.slice());
|
|
867
|
+
const autoMain: Array<[number, number]> = [];
|
|
868
|
+
if (slack > 0) {
|
|
869
|
+
for (const index of line) {
|
|
870
|
+
const edges = margins[index];
|
|
871
|
+
if (row) {
|
|
872
|
+
if (edges[3] === "auto") autoMain.push([index, 3]);
|
|
873
|
+
if (edges[1] === "auto") autoMain.push([index, 1]);
|
|
874
|
+
} else {
|
|
875
|
+
if (edges[0] === "auto") autoMain.push([index, 0]);
|
|
876
|
+
if (edges[2] === "auto") autoMain.push([index, 2]);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
544
879
|
}
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
880
|
+
const justifySlack = autoMain.length === 0 ? slack : 0;
|
|
881
|
+
const autoMainShares = distribute(
|
|
882
|
+
slack,
|
|
883
|
+
autoMain.map(() => 1),
|
|
884
|
+
);
|
|
885
|
+
for (let index = 0; index < autoMain.length; index += 1) {
|
|
886
|
+
const [childIndex, edge] = autoMain[index];
|
|
887
|
+
placed[childIndex][edge] += autoMainShares[index];
|
|
550
888
|
}
|
|
551
|
-
}
|
|
552
889
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
} else if (justify === "flex-end") {
|
|
571
|
-
cursor = slack;
|
|
572
|
-
} else if (justify === "space-between" && children.length > 1) {
|
|
573
|
-
between = gap + Math.floor(slack / (children.length - 1));
|
|
574
|
-
} else if (justify === "space-around" && children.length > 0) {
|
|
575
|
-
const each = Math.floor(slack / children.length);
|
|
576
|
-
cursor = Math.floor(each / 2);
|
|
577
|
-
between = gap + each;
|
|
578
|
-
} else if (justify === "space-evenly" && children.length > 0) {
|
|
579
|
-
const each = Math.floor(slack / (children.length + 1));
|
|
580
|
-
cursor = each;
|
|
581
|
-
between = gap + each;
|
|
582
|
-
}
|
|
890
|
+
let cursor = 0;
|
|
891
|
+
let between = gap;
|
|
892
|
+
if (justify === "center") {
|
|
893
|
+
cursor = Math.floor(justifySlack / 2);
|
|
894
|
+
} else if (justify === "flex-end") {
|
|
895
|
+
cursor = justifySlack;
|
|
896
|
+
} else if (justify === "space-between" && line.length > 1) {
|
|
897
|
+
between = gap + Math.floor(justifySlack / (line.length - 1));
|
|
898
|
+
} else if (justify === "space-around" && line.length > 0) {
|
|
899
|
+
const each = Math.floor(justifySlack / line.length);
|
|
900
|
+
cursor = Math.floor(each / 2);
|
|
901
|
+
between = gap + each;
|
|
902
|
+
} else if (justify === "space-evenly" && line.length > 0) {
|
|
903
|
+
const each = Math.floor(justifySlack / (line.length + 1));
|
|
904
|
+
cursor = each;
|
|
905
|
+
between = gap + each;
|
|
906
|
+
}
|
|
583
907
|
|
|
584
|
-
|
|
585
|
-
const parentAlign = style.alignItems ?? "stretch";
|
|
908
|
+
const order = reverse ? line.slice().reverse() : line;
|
|
586
909
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
910
|
+
for (const index of order) {
|
|
911
|
+
const child = children[index];
|
|
912
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = placed[index];
|
|
913
|
+
const mainMarginStart = row ? marginLeft : marginTop;
|
|
914
|
+
const mainMarginEnd = row ? marginRight : marginBottom;
|
|
915
|
+
const crossMarginStart = row ? marginTop : marginLeft;
|
|
916
|
+
const crossMarginEnd = row ? marginBottom : marginRight;
|
|
917
|
+
const crossStartEdge = row ? 0 : 3;
|
|
918
|
+
const crossEndEdge = row ? 2 : 1;
|
|
919
|
+
const autoCrossStart = margins[index][crossStartEdge] === "auto";
|
|
920
|
+
const autoCrossEnd = margins[index][crossEndEdge] === "auto";
|
|
921
|
+
const hasAutoCross = autoCrossStart || autoCrossEnd;
|
|
922
|
+
|
|
923
|
+
const align = (() => {
|
|
924
|
+
const own = child.style.alignSelf ?? "auto";
|
|
925
|
+
return own === "auto" ? parentAlign : own;
|
|
926
|
+
})();
|
|
927
|
+
|
|
928
|
+
const crossAvailable = Math.max(0, lineCross - crossMarginStart - crossMarginEnd);
|
|
929
|
+
const fixedCross = resolve(row ? child.style.height : child.style.width, crossSpace);
|
|
930
|
+
let crossSize: number;
|
|
931
|
+
if (fixedCross != null) {
|
|
932
|
+
crossSize = fixedCross;
|
|
933
|
+
} else if (align === "stretch" && !hasAutoCross) {
|
|
934
|
+
crossSize = crossAvailable;
|
|
935
|
+
} else {
|
|
936
|
+
const size = intrinsicSize(
|
|
937
|
+
child,
|
|
938
|
+
row ? mainSizes[index] : crossAvailable,
|
|
939
|
+
row ? crossAvailable : mainSizes[index],
|
|
940
|
+
);
|
|
941
|
+
crossSize = row ? size.height : size.width;
|
|
942
|
+
}
|
|
943
|
+
crossSize = Math.min(crossSize, crossAvailable);
|
|
944
|
+
|
|
945
|
+
let crossOffset = crossOrigin + crossMarginStart;
|
|
946
|
+
if (hasAutoCross) {
|
|
947
|
+
const remaining = Math.max(0, lineCross - crossSize - crossMarginStart - crossMarginEnd);
|
|
948
|
+
const autoCrossShares = distribute(remaining, [
|
|
949
|
+
autoCrossStart ? 1 : 0,
|
|
950
|
+
autoCrossEnd ? 1 : 0,
|
|
951
|
+
]);
|
|
952
|
+
crossOffset += autoCrossShares[0];
|
|
953
|
+
} else if (align === "center") {
|
|
954
|
+
crossOffset += Math.floor((crossAvailable - crossSize) / 2);
|
|
955
|
+
} else if (align === "flex-end") {
|
|
956
|
+
crossOffset += crossAvailable - crossSize;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const mainStart = cursor + mainMarginStart;
|
|
960
|
+
const childWidth = row ? mainSizes[index] : crossSize;
|
|
961
|
+
const childHeight = row ? crossSize : mainSizes[index];
|
|
962
|
+
const [shiftX, shiftY] = relativeShift(child.style, contentWidth, contentHeight);
|
|
963
|
+
const childX = (row ? contentX + mainStart : contentX + crossOffset) + shiftX;
|
|
964
|
+
const childY = (row ? contentY + crossOffset : contentY + mainStart) + shiftY;
|
|
965
|
+
|
|
966
|
+
layout(
|
|
609
967
|
child,
|
|
610
|
-
|
|
611
|
-
|
|
968
|
+
childX,
|
|
969
|
+
childY,
|
|
970
|
+
clampDimension(childWidth, child.style.minWidth, child.style.maxWidth, contentWidth),
|
|
971
|
+
clampDimension(childHeight, child.style.minHeight, child.style.maxHeight, contentHeight),
|
|
972
|
+
block,
|
|
612
973
|
);
|
|
613
|
-
|
|
974
|
+
|
|
975
|
+
cursor = mainStart + mainSizes[index] + mainMarginEnd + between;
|
|
614
976
|
}
|
|
615
|
-
|
|
977
|
+
};
|
|
616
978
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
979
|
+
if (!wraps(style)) {
|
|
980
|
+
placeLine(
|
|
981
|
+
children.map((_, index) => index),
|
|
982
|
+
0,
|
|
983
|
+
crossSpace,
|
|
984
|
+
);
|
|
985
|
+
} else {
|
|
986
|
+
// Lines are filled in order until the next child's outer size would not
|
|
987
|
+
// fit, and a child wider than the whole line gets a line of its own
|
|
988
|
+
// rather than none. Each line is as deep as its deepest child asks to be,
|
|
989
|
+
// measured at the main size it started with.
|
|
990
|
+
const lines = breakLines(
|
|
991
|
+
bases.map((_, index) => outerMain(index)),
|
|
992
|
+
mainSpace,
|
|
993
|
+
gap,
|
|
994
|
+
);
|
|
995
|
+
const depths = lines.map((line) =>
|
|
996
|
+
line.reduce((deepest, index) => {
|
|
997
|
+
const child = children[index];
|
|
998
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = resolvedMargins[index];
|
|
999
|
+
const fixedCross = resolve(row ? child.style.height : child.style.width, crossSpace);
|
|
1000
|
+
const cross =
|
|
1001
|
+
fixedCross ??
|
|
1002
|
+
(row
|
|
1003
|
+
? intrinsicSize(child, bases[index], Math.max(0, crossSpace - marginTop - marginBottom))
|
|
1004
|
+
.height
|
|
1005
|
+
: intrinsicSize(child, Math.max(0, crossSpace - marginLeft - marginRight), bases[index])
|
|
1006
|
+
.width);
|
|
1007
|
+
return Math.max(
|
|
1008
|
+
deepest,
|
|
1009
|
+
cross + (row ? marginTop + marginBottom : marginLeft + marginRight),
|
|
1010
|
+
);
|
|
1011
|
+
}, 0),
|
|
1012
|
+
);
|
|
1013
|
+
const lineGap = gapOf(style, !row);
|
|
1014
|
+
const spare =
|
|
1015
|
+
crossSpace -
|
|
1016
|
+
depths.reduce((total, depth) => total + depth, 0) -
|
|
1017
|
+
Math.max(0, lines.length - 1) * lineGap;
|
|
1018
|
+
const [first, between, stretch] = alignLines(style.alignContent, spare, lines.length);
|
|
1019
|
+
const extra = distribute(
|
|
1020
|
+
stretch,
|
|
1021
|
+
lines.map(() => 1),
|
|
1022
|
+
);
|
|
1023
|
+
let origin = first;
|
|
1024
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1025
|
+
const depth = depths[index] + (extra[index] ?? 0);
|
|
1026
|
+
// `wrap-reverse` stacks the lines from the far edge of the cross axis,
|
|
1027
|
+
// and keeps each line's own contents the right way round.
|
|
1028
|
+
const at = style.flexWrap === "wrap-reverse" ? crossSpace - origin - depth : origin;
|
|
1029
|
+
placeLine(lines[index], at, depth);
|
|
1030
|
+
origin += depth + lineGap + between;
|
|
622
1031
|
}
|
|
1032
|
+
}
|
|
623
1033
|
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1034
|
+
if (children !== node.children) {
|
|
1035
|
+
for (const child of node.children) {
|
|
1036
|
+
if (isAbsolute(child)) {
|
|
1037
|
+
layoutAbsolute(node, child, block);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
629
1042
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1043
|
+
/**
|
|
1044
|
+
* Place a child that is out of its parent's line.
|
|
1045
|
+
*
|
|
1046
|
+
* Its containing block is the *padding* box — inside the border, not inside
|
|
1047
|
+
* the padding — of its nearest positioned ancestor, which is CSS's rule and
|
|
1048
|
+
* Yoga's, and the reason `top: 0` puts it on the first row inside a frame
|
|
1049
|
+
* rather than on the frame. Every node is positioned unless it says
|
|
1050
|
+
* `position: "static"`, as in OpenTUI, so that ancestor is the parent unless a
|
|
1051
|
+
* static box is in the way.
|
|
1052
|
+
*
|
|
1053
|
+
* Each axis is decided the same way. A size given is that size, and a
|
|
1054
|
+
* percentage is of the containing block. Otherwise both offsets on an axis
|
|
1055
|
+
* stretch it between them, and one or none leaves it the size of its content.
|
|
1056
|
+
* The start offset places it, or failing that the end one; with neither, it
|
|
1057
|
+
* goes where its *parent's* `justifyContent` and `alignItems` would have put a
|
|
1058
|
+
* lone child — Yoga's static position, which is about the line the box was
|
|
1059
|
+
* taken out of and so is the parent's even when the containing block is
|
|
1060
|
+
* further up — and not at the corner, which is what makes `position:
|
|
1061
|
+
* "absolute"` with no offsets an overlay centred by the same props that
|
|
1062
|
+
* centre anything else.
|
|
1063
|
+
*/
|
|
1064
|
+
function layoutAbsolute(parent: LayoutNode, child: LayoutNode, block: ContainingBlock): void {
|
|
1065
|
+
const boxX = block.x;
|
|
1066
|
+
const boxY = block.y;
|
|
1067
|
+
const boxWidth = block.width;
|
|
1068
|
+
const boxHeight = block.height;
|
|
1069
|
+
const style = child.style;
|
|
1070
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = fixedMargins(margin(style));
|
|
1071
|
+
const left = resolveOffset(style.left, boxWidth);
|
|
1072
|
+
const right = resolveOffset(style.right, boxWidth);
|
|
1073
|
+
const top = resolveOffset(style.top, boxHeight);
|
|
1074
|
+
const bottom = resolveOffset(style.bottom, boxHeight);
|
|
1075
|
+
|
|
1076
|
+
const spanWidth = Math.max(0, boxWidth - (left ?? 0) - (right ?? 0) - marginLeft - marginRight);
|
|
1077
|
+
const spanHeight = Math.max(0, boxHeight - (top ?? 0) - (bottom ?? 0) - marginTop - marginBottom);
|
|
1078
|
+
let childWidth = resolve(style.width, boxWidth);
|
|
1079
|
+
let childHeight = resolve(style.height, boxHeight);
|
|
1080
|
+
if (childWidth == null && left != null && right != null) {
|
|
1081
|
+
childWidth = spanWidth;
|
|
1082
|
+
}
|
|
1083
|
+
if (childHeight == null && top != null && bottom != null) {
|
|
1084
|
+
childHeight = spanHeight;
|
|
1085
|
+
}
|
|
1086
|
+
if (childWidth == null || childHeight == null) {
|
|
1087
|
+
const size = intrinsicSize(child, childWidth ?? spanWidth, childHeight ?? spanHeight);
|
|
1088
|
+
childWidth = childWidth ?? Math.min(size.width, spanWidth);
|
|
1089
|
+
childHeight = childHeight ?? size.height;
|
|
1090
|
+
}
|
|
1091
|
+
childWidth = clampDimension(childWidth, style.minWidth, style.maxWidth, boxWidth);
|
|
1092
|
+
childHeight = clampDimension(childHeight, style.minHeight, style.maxHeight, boxHeight);
|
|
1093
|
+
|
|
1094
|
+
// The static position, for an axis with no offset on it: where the parent's
|
|
1095
|
+
// own alignment would put a child of this size inside its padding.
|
|
1096
|
+
const own = paddingBox(parent);
|
|
1097
|
+
const [padTop, padRight, padBottom, padLeft] = padding(parent.style);
|
|
1098
|
+
const flexDirection = direction(parent.style);
|
|
1099
|
+
const row = isRow(flexDirection);
|
|
1100
|
+
const reverse = flexDirection === "row-reverse" || flexDirection === "column-reverse";
|
|
1101
|
+
const place = (
|
|
1102
|
+
alignment: string,
|
|
1103
|
+
flipped: boolean,
|
|
1104
|
+
start: number,
|
|
1105
|
+
space: number,
|
|
1106
|
+
size: number,
|
|
1107
|
+
marginStart: number,
|
|
1108
|
+
marginEnd: number,
|
|
1109
|
+
): number => {
|
|
1110
|
+
const free = space - size - marginStart - marginEnd;
|
|
1111
|
+
const toEnd = alignment === "flex-end" ? !flipped : alignment !== "center" && flipped;
|
|
1112
|
+
if (alignment === "center") {
|
|
1113
|
+
return start + marginStart + Math.floor(free / 2);
|
|
1114
|
+
}
|
|
1115
|
+
return toEnd ? start + marginStart + free : start + marginStart;
|
|
1116
|
+
};
|
|
1117
|
+
const justify = parent.style.justifyContent ?? "flex-start";
|
|
1118
|
+
const ownAlign = style.alignSelf ?? "auto";
|
|
1119
|
+
const align = ownAlign === "auto" ? (parent.style.alignItems ?? "stretch") : ownAlign;
|
|
1120
|
+
const innerWidth = Math.max(0, own.width - padLeft - padRight);
|
|
1121
|
+
const innerHeight = Math.max(0, own.height - padTop - padBottom);
|
|
1122
|
+
const staticX = row
|
|
1123
|
+
? place(justify, reverse, own.x + padLeft, innerWidth, childWidth, marginLeft, marginRight)
|
|
1124
|
+
: place(align, false, own.x + padLeft, innerWidth, childWidth, marginLeft, marginRight);
|
|
1125
|
+
const staticY = row
|
|
1126
|
+
? place(align, false, own.y + padTop, innerHeight, childHeight, marginTop, marginBottom)
|
|
1127
|
+
: place(justify, reverse, own.y + padTop, innerHeight, childHeight, marginTop, marginBottom);
|
|
637
1128
|
|
|
638
|
-
|
|
1129
|
+
let childX = staticX;
|
|
1130
|
+
if (left != null) {
|
|
1131
|
+
childX = boxX + left + marginLeft;
|
|
1132
|
+
} else if (right != null) {
|
|
1133
|
+
childX = boxX + boxWidth - right - marginRight - childWidth;
|
|
1134
|
+
}
|
|
1135
|
+
let childY = staticY;
|
|
1136
|
+
if (top != null) {
|
|
1137
|
+
childY = boxY + top + marginTop;
|
|
1138
|
+
} else if (bottom != null) {
|
|
1139
|
+
childY = boxY + boxHeight - bottom - marginBottom - childHeight;
|
|
639
1140
|
}
|
|
1141
|
+
layout(child, childX, childY, childWidth, childHeight, block);
|
|
640
1142
|
}
|
|
641
1143
|
|
|
642
1144
|
/**
|
|
@@ -668,7 +1170,14 @@ export function layout(
|
|
|
668
1170
|
* seventy-six are — which is the property the whole component exists for, and
|
|
669
1171
|
* the reason this is not `overflow: "hidden"` with a margin on top.
|
|
670
1172
|
*/
|
|
671
|
-
function layoutScroll(
|
|
1173
|
+
function layoutScroll(
|
|
1174
|
+
node: LayoutNode,
|
|
1175
|
+
x: number,
|
|
1176
|
+
y: number,
|
|
1177
|
+
width: number,
|
|
1178
|
+
height: number,
|
|
1179
|
+
block: ContainingBlock,
|
|
1180
|
+
): void {
|
|
672
1181
|
const children = node.children;
|
|
673
1182
|
const stack = scrollStack(node, width, height);
|
|
674
1183
|
const content = stack.content;
|
|
@@ -696,10 +1205,10 @@ function layoutScroll(node: LayoutNode, x: number, y: number, width: number, hei
|
|
|
696
1205
|
break;
|
|
697
1206
|
}
|
|
698
1207
|
const child = children[index];
|
|
699
|
-
const [, marginRight, , marginLeft] = margin(child.style);
|
|
1208
|
+
const [, marginRight, , marginLeft] = fixedMargins(margin(child.style));
|
|
700
1209
|
const available = Math.max(0, width - marginLeft - marginRight);
|
|
701
1210
|
const childWidth = Math.min(available, resolve(child.style.width, width) ?? available);
|
|
702
|
-
layout(child, x + marginLeft, y + top - offset, childWidth, stack.heights[index]);
|
|
1211
|
+
layout(child, x + marginLeft, y + top - offset, childWidth, stack.heights[index], block);
|
|
703
1212
|
count += 1;
|
|
704
1213
|
}
|
|
705
1214
|
node.scrollFirst = first;
|
|
@@ -747,7 +1256,9 @@ function scrollStack(node: LayoutNode, width: number, height: number): ScrollInd
|
|
|
747
1256
|
const gap = gapOf(node.style, false);
|
|
748
1257
|
let stack = node.scrollIndex;
|
|
749
1258
|
if (stack == null || stack.width !== width || stack.view !== height || stack.gap !== gap) {
|
|
750
|
-
|
|
1259
|
+
// Annotated rather than inferred: a literal's `from: 0` would otherwise
|
|
1260
|
+
// be typed as the number zero for the rest of this function.
|
|
1261
|
+
const fresh: ScrollIndex = {
|
|
751
1262
|
width,
|
|
752
1263
|
view: height,
|
|
753
1264
|
gap,
|
|
@@ -756,6 +1267,7 @@ function scrollStack(node: LayoutNode, width: number, height: number): ScrollInd
|
|
|
756
1267
|
heights: [],
|
|
757
1268
|
content: 0,
|
|
758
1269
|
};
|
|
1270
|
+
stack = fresh;
|
|
759
1271
|
node.scrollIndex = stack;
|
|
760
1272
|
}
|
|
761
1273
|
|
|
@@ -796,15 +1308,15 @@ function scrollStack(node: LayoutNode, width: number, height: number): ScrollInd
|
|
|
796
1308
|
let cursor = 0;
|
|
797
1309
|
if (stack.from > 0) {
|
|
798
1310
|
const previous = children[stack.from - 1];
|
|
799
|
-
const [, , previousBottom] = margin(previous.style);
|
|
1311
|
+
const [, , previousBottom] = fixedMargins(margin(previous.style));
|
|
800
1312
|
cursor = stack.tops[stack.from - 1] + stack.heights[stack.from - 1] + previousBottom + gap;
|
|
801
1313
|
}
|
|
802
1314
|
for (let index = stack.from; index < children.length; index += 1) {
|
|
803
1315
|
const child = children[index];
|
|
804
|
-
const [marginTop, marginRight, marginBottom, marginLeft] = margin(child.style);
|
|
1316
|
+
const [marginTop, marginRight, marginBottom, marginLeft] = fixedMargins(margin(child.style));
|
|
805
1317
|
const available = Math.max(0, width - marginLeft - marginRight);
|
|
806
|
-
const
|
|
807
|
-
const own =
|
|
1318
|
+
const fixedHeight = resolve(child.style.height, height);
|
|
1319
|
+
const own = fixedHeight ?? intrinsicSize(child, available, unbounded).height;
|
|
808
1320
|
stack.tops[index] = cursor + marginTop;
|
|
809
1321
|
stack.heights[index] = own;
|
|
810
1322
|
cursor += marginTop + own + marginBottom + gap;
|
|
@@ -819,15 +1331,24 @@ function scrollStack(node: LayoutNode, width: number, height: number): ScrollInd
|
|
|
819
1331
|
return stack;
|
|
820
1332
|
}
|
|
821
1333
|
|
|
1334
|
+
// Implemented, and asserted as cells in `npm/tui/tui.test.js`: `flexWrap`
|
|
1335
|
+
// with `alignContent`; `auto` margins on both axes; and `position` in all
|
|
1336
|
+
// three of Yoga 3's values, with an absolute box placed against the padding
|
|
1337
|
+
// box of its nearest ancestor that is not `"static"` (or the root). Paint, and
|
|
1338
|
+
// so the hit grid, still follow the *tree*: an absolute box is drawn when its
|
|
1339
|
+
// parent is, under its parent's `zIndex` order and inside its parent's clip —
|
|
1340
|
+
// which is OpenTUI's rule, and CSS's is different: there, `overflow: hidden`
|
|
1341
|
+
// on a static box between an absolute one and its containing block does not
|
|
1342
|
+
// clip it. Here it does.
|
|
1343
|
+
//
|
|
822
1344
|
// Not implemented here, on purpose, and tracked rather than discovered:
|
|
823
1345
|
//
|
|
824
|
-
// - `
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
// `justifyContent: "center"` already covers here.
|
|
1346
|
+
// - `aspectRatio`. A terminal resolves whole cells, and nothing here has a
|
|
1347
|
+
// fractional layout pass to lean on.
|
|
1348
|
+
// - `position` and `flexWrap` inside a scrolling box. Its children are one
|
|
1349
|
+
// column whose heights are kept between frames (see {@link ScrollIndex}),
|
|
1350
|
+
// and a child that is out of it, drawn somewhere other than where it says,
|
|
1351
|
+
// or sharing a row with another, is a second answer to "which rows are in
|
|
1352
|
+
// the window". Both are read there as if they were not given.
|
|
832
1353
|
//
|
|
833
|
-
//
|
|
1354
|
+
// Both are ubugeeei-prod/uf#314.
|