@react-x11/components 0.14.0 → 0.16.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.
Files changed (97) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +209 -23
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +873 -175
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +62 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -0
  22. package/dist/html/css/shapes.js +214 -0
  23. package/dist/html/css/shapes.js.map +1 -0
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +75 -27
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +38 -7
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +160 -50
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +29 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +159 -20
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +69 -19
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1648 -225
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +23 -8
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +907 -323
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +36 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +174 -37
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts +4 -1
  64. package/dist/html/paint.d.ts.map +1 -1
  65. package/dist/html/paint.js +769 -204
  66. package/dist/html/paint.js.map +1 -1
  67. package/dist/html/resources.js +14 -1
  68. package/dist/html/resources.js.map +1 -1
  69. package/dist/html/svg.d.ts +50 -17
  70. package/dist/html/svg.d.ts.map +1 -1
  71. package/dist/html/svg.js +501 -83
  72. package/dist/html/svg.js.map +1 -1
  73. package/dist/html/widgets.d.ts.map +1 -1
  74. package/dist/html/widgets.js +57 -4
  75. package/dist/html/widgets.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/code-editor/node.ts +24 -5
  78. package/src/html/css/cascade.ts +987 -176
  79. package/src/html/css/color.ts +345 -18
  80. package/src/html/css/content.ts +173 -15
  81. package/src/html/css/parse.ts +60 -8
  82. package/src/html/css/shapes.ts +273 -0
  83. package/src/html/css/style.ts +123 -30
  84. package/src/html/css/ua.ts +15 -0
  85. package/src/html/css/values.ts +23 -9
  86. package/src/html/index.ts +7 -0
  87. package/src/html/layout/block.ts +169 -56
  88. package/src/html/layout/boxes.ts +198 -24
  89. package/src/html/layout/css-grid.ts +89 -22
  90. package/src/html/layout/flex.ts +1863 -219
  91. package/src/html/layout/inline.ts +27 -7
  92. package/src/html/layout/table.ts +989 -333
  93. package/src/html/node.ts +204 -37
  94. package/src/html/paint.ts +999 -252
  95. package/src/html/resources.ts +13 -1
  96. package/src/html/svg.ts +600 -78
  97. package/src/html/widgets.ts +61 -4
@@ -34,6 +34,7 @@ import { Box, PAINT_ORDER, isBlank } from './boxes.js';
34
34
  import {
35
35
  FLEXED_HEIGHT,
36
36
  MIN_CONTENT_PROBE,
37
+ STRETCHED_ACROSS,
37
38
  USED_HEIGHT,
38
39
  centreButton,
39
40
  clampHeight,
@@ -50,11 +51,13 @@ import {
50
51
  transferredHeight,
51
52
  transferredWidth,
52
53
  heightFromWidth,
54
+ heightThroughRatio,
53
55
  widthFromHeight,
54
56
  resolveEdges,
55
57
  } from './block.js';
56
58
  import { layoutGrid } from './css-grid.js';
57
59
  import { firstBaselineIn } from './inline.js';
60
+ import { TABLE_CONTENT } from './table.js';
58
61
  import type { LayoutContext } from './block.js';
59
62
 
60
63
  // `react-x11/yoga` re-exports yoga's own declarations, so the node shape and
@@ -121,7 +124,6 @@ export function layoutFlex(
121
124
  // and the measure that asked came back vast: a row beside such an item
122
125
  // was squeezed to wrap every word.
123
126
  const bounded = Number.isFinite(contentWidth);
124
- if (bounded) root.setWidth(contentWidth);
125
127
  // The content box's height, where it is definite: a length or a
126
128
  // percentage that resolves, or what `aspect-ratio` makes of the width —
127
129
  // less the padding and borders a `border-box` height holds, which put
@@ -132,18 +134,22 @@ export function layoutFlex(
132
134
  const definite = percentBaseInside(box);
133
135
  const own = Number.isFinite(definite);
134
136
  const height = USED_HEIGHT.get(box) ?? (own ? definite : null);
135
- // whether its content may come to more than it is tall
136
- let capped = height !== null;
137
+ // the limits on its content box's height where it has none of its own
138
+ let floor = 0;
139
+ let ceiling = Infinity;
137
140
  if (height !== null) root.setHeight(height);
138
141
  else {
139
142
  root.setHeightAuto();
140
143
  const extra = box.verticalExtra;
141
144
  const min = clampHeight(box, extra) - extra;
142
- if (min > 0) root.setMinHeight(min);
145
+ if (min > 0) {
146
+ floor = min;
147
+ root.setMinHeight(min);
148
+ }
143
149
  const max = clampHeight(box, Infinity) - extra;
144
150
  if (Number.isFinite(max)) {
145
- root.setMaxHeight(Math.max(0, max));
146
- capped = true;
151
+ ceiling = Math.max(0, max);
152
+ root.setMaxHeight(ceiling);
147
153
  }
148
154
  }
149
155
 
@@ -169,6 +175,17 @@ export function layoutFlex(
169
175
  );
170
176
  } else PAINT_ORDER.delete(box);
171
177
 
178
+ // Whether a line weighs its items one against another as it shrinks
179
+ // them, each by its flex base size (CSS Flexbox 9.7, step 4c): the one
180
+ // line of a row that does not wrap, two or more of whose items may
181
+ // shrink. One that shrinks alone comes to the room the rest leave it,
182
+ // whatever its base, and a line of a row that wraps is too narrow for
183
+ // an item only where that is alone on it.
184
+ const weighs =
185
+ box.style.flexWrap === 'nowrap' &&
186
+ box.style.flexDirection.startsWith('row') &&
187
+ flowing.filter((child) => child.style.flexShrink > 0).length > 1;
188
+
172
189
  const items: { box: Box; node: YogaNode; laid: Laid }[] = [];
173
190
  for (const child of flowing) {
174
191
  const node = Y.Node.create(flexConfig());
@@ -181,17 +198,73 @@ export function layoutFlex(
181
198
  stretch: NaN,
182
199
  main: NaN,
183
200
  least: NaN,
201
+ most: NaN,
202
+ basis: NaN,
203
+ sized: false,
204
+ content: NaN,
205
+ fitted: NaN,
206
+ base: NaN,
207
+ narrowest: NaN,
184
208
  held: NaN,
209
+ minimum: NaN,
185
210
  auto: false,
211
+ across: false,
212
+ unstretched: NaN,
186
213
  };
187
- applyItem(node, child, ctx, contentWidth, laid, height !== null, own);
214
+ applyItem(
215
+ node,
216
+ child,
217
+ ctx,
218
+ contentWidth,
219
+ laid,
220
+ height !== null,
221
+ own ? definite : NaN,
222
+ weighs,
223
+ );
188
224
  root.insertChild(node, items.length);
189
225
  items.push({ box: child, node, laid });
190
226
  }
191
227
 
228
+ // A column that wraps is as wide as its content box, but each of its
229
+ // lines is as wide as its widest item, every item `fit-content` across
230
+ // (CSS Flexbox 9.4, steps 7 and 8), and so is placed across here and not
231
+ // by Yoga (`crossLines`). Yoga stretched an item to the box wherever the
232
+ // column had room for every item down it — with no height, always — so a
233
+ // line was the box's width, an item wider at its narrowest ran out of
234
+ // it, and `align-content` had no room to centre or end a line in. And
235
+ // stretching a line, it laid each stretched item out again at its height
236
+ // less its top and bottom margins and plus its side ones (its multi-line
237
+ // pass adds the margins across a column where it means the ones down
238
+ // it), and added what `space-around` puts between lines to its width. So
239
+ // Yoga lays the box out with every item at its line's start, where it
240
+ // measures each `fit-content` (`innerWidth`) and down the column as wide
241
+ // as that, as the specification measures a line's items (9.2, step 3),
242
+ // and the lines one more pixel apart than they are, so that each line
243
+ // starts where the last does not and says which items are on it.
244
+ const crossed = wrapsColumn(box.style, contentWidth);
245
+ if (crossed) {
246
+ root.setAlignContent(Y.ALIGN_FLEX_START);
247
+ root.setGap(Y.GUTTER_COLUMN, columnGap + 1);
248
+ for (const { node } of items) node.setAlignSelf(Y.ALIGN_FLEX_START);
249
+ }
250
+ if (bounded) root.setWidth(contentWidth);
251
+
192
252
  const direction =
193
253
  box.style.direction === 'rtl' ? Y.DIRECTION_RTL : Y.DIRECTION_LTR;
194
254
  const row = box.style.flexDirection.startsWith('row');
255
+ // An `auto` margin across a row that wraps takes the room its line has
256
+ // past the item (CSS Flexbox 8.1, 9.6), where Yoga's pass that sets the
257
+ // items across their lines reads an item's alignment and not its
258
+ // margins: it stretched one with `margin-top: auto` to its line, and set
259
+ // one with a height at its line's start. So Yoga is told the alignment
260
+ // the margins come to (`autoAcross`), which it then places it by.
261
+ if (row && box.style.flexWrap !== 'nowrap') {
262
+ const reverse = box.style.flexWrap === 'wrap-reverse';
263
+ for (const { box: child, node } of items) {
264
+ const aligned = autoAcross(child.style, reverse);
265
+ if (aligned) node.setAlignSelf(ALIGN[aligned]);
266
+ }
267
+ }
195
268
  // frozen at its least size on the one line of a box that does not
196
269
  // wrap, by a basis where Yoga takes one along the main axis
197
270
  // (`applyItem`'s `setBasis`)
@@ -199,30 +272,80 @@ export function layoutFlex(
199
272
  frozen: box.style.flexWrap === 'nowrap',
200
273
  basis: row ? bounded : height !== null,
201
274
  };
202
- const ask = (): void =>
275
+ const layout = (): void =>
203
276
  root.calculateLayout(
204
277
  bounded ? contentWidth : Number.NaN,
205
278
  height ?? Number.NaN,
206
279
  direction,
207
280
  );
208
- // the one line of a box that does not wrap may be short of room, where
209
- // the box has a size along it that its items do not make: and Yoga's
210
- // answer for a line every item of which its minimum stops is put right
211
- // (`stoppedLine`)
212
- const short = hold.frozen && (row ? bounded : capped);
281
+ // Down a column that wraps, the items Yoga stretches across their lines
282
+ // come out of its layout at another height than it sized them at, where
283
+ // their side margins are not their top and bottom ones (`unstretch`):
284
+ // laid out so that each has the height it was sized at
285
+ const restretched = row || hold.frozen ? [] : stretchedDown(root, items);
286
+ const sized = bounded ? contentWidth : null;
287
+ // and across a row that wraps, the items Yoga stretches across lines
288
+ // `align-content` spaces out come out of its layout as tall as their
289
+ // line and the room after it as well (`unspace`): laid out so that each
290
+ // is as tall as its line, where the box has room to space them out in —
291
+ // a height, or a minimum one
292
+ const spaced =
293
+ row &&
294
+ bounded &&
295
+ !hold.frozen &&
296
+ (height !== null || floor > 0) &&
297
+ spacesLines(box.style);
298
+ const ask =
299
+ restretched.length > 0
300
+ ? (): void => unstretch(items, restretched, sized, layout)
301
+ : spaced
302
+ ? (): void =>
303
+ unspace(root, items, box.style.flexWrap === 'wrap-reverse', layout)
304
+ : layout;
305
+ // The one line of a box that does not wrap is shared out by CSS
306
+ // Flexbox 9.7 where Yoga's answer for it may not be what that makes of
307
+ // it (`resolveLine`): along a row of a width, or down a column of a
308
+ // height or of limits on one, where a line may have room to share out
309
+ const line: FlexLine | null =
310
+ hold.frozen &&
311
+ (row ? bounded : height !== null || floor > 0 || ceiling < Infinity)
312
+ ? {
313
+ resolved: false,
314
+ size: row ? contentWidth : (height ?? NaN),
315
+ floor,
316
+ ceiling,
317
+ between: betweenOf(items, row, row ? columnGap : rowGap),
318
+ probe: row && contentWidth === MIN_CONTENT_PROBE,
319
+ }
320
+ : null;
321
+ // and the lines of a box that wraps may have room left over, where a
322
+ // row has a width to have it in: Yoga's answer for one every item of
323
+ // which that may grow its maximum stops is put right (`cappedLines`) —
324
+ // broken into lines as Yoga breaks them, at the box's size along them
325
+ const roomy = !hold.frozen && (row ? bounded : true);
326
+ const breaks = row ? contentWidth : (height ?? Infinity);
213
327
  // each item's size along the main axis, read the once a layout (`Laid`)
214
328
  const read = (): void => {
215
329
  for (const { node, laid } of items) {
216
- laid.main = row ? node.getComputedWidth() : node.getComputedHeight();
330
+ laid.main = row ? node.getComputedWidth() : heightOf(node, laid);
217
331
  }
218
332
  };
219
333
  const calculate = (): void => {
334
+ // a line shared out here is again, before Yoga places it: an item
335
+ // since held at its content's size is frozen there (`autoMinimums`)
336
+ if (line?.resolved) shareOut(items, row, line, hold);
220
337
  ask();
221
338
  read();
222
- if (!short) return;
223
- const room = row ? contentWidth : (height ?? root.getComputedHeight());
224
- const gap = row ? columnGap : rowGap;
225
- if (stoppedLine(items, row, room, gap, hold, ask)) read();
339
+ if (line && !line.resolved) {
340
+ const room = Number.isNaN(line.size)
341
+ ? root.getComputedHeight()
342
+ : line.size;
343
+ if (resolveLine(items, row, line, room, hold, ask)) read();
344
+ } else if (roomy) {
345
+ const room = row ? contentWidth : (height ?? root.getComputedHeight());
346
+ const gap = row ? columnGap : rowGap;
347
+ if (cappedLines(items, row, room, breaks, gap, hold, ask)) read();
348
+ }
226
349
  };
227
350
  ctx.flexDepth = depth + 1;
228
351
  try {
@@ -241,6 +364,51 @@ export function layoutFlex(
241
364
  ctx.flexDepth = depth;
242
365
  }
243
366
 
367
+ // The lines of a row that wraps are as tall as their tallest items, each
368
+ // as tall as its content across (CSS Flexbox 9.4, steps 7 and 8), and
369
+ // `align-content` stretches lines short of the box to it, or centres
370
+ // them, or ends them, past its edges where they are taller (step 9).
371
+ // Handed the box's height as a size, Yoga measures a stretched item at it
372
+ // wherever the row has room along it for every item, and so its line is
373
+ // the box's height: an item taller than the box was squashed to it, and a
374
+ // line `align-content` centres stayed at the box's top. Where that is so
375
+ // (`heldLine`), the box is held to its height from both sides instead,
376
+ // and laid out again: Yoga measures each item as at most that tall, which
377
+ // its content passes where it is taller — the maximum a percentage, as a
378
+ // minimum and a maximum that are the same are a size to Yoga. Only there,
379
+ // which takes in the one line `space-around` and `space-evenly` centre,
380
+ // and not where the row is measured for its widest, which is laid out
381
+ // for its width alone.
382
+ if (
383
+ row &&
384
+ bounded &&
385
+ height !== null &&
386
+ box.style.flexWrap !== 'nowrap' &&
387
+ heldLine(box, ctx, items, height, contentWidth, columnGap)
388
+ ) {
389
+ root.setHeightAuto();
390
+ root.setMinHeight(height);
391
+ root.setMaxHeightPercent(100);
392
+ // and a percentage height resolved: Yoga takes one inside a box it
393
+ // does not hold to a size for none, and measured `h-1/2` as tall as
394
+ // its content
395
+ if (own) {
396
+ for (const { box: child, node } of items) {
397
+ const set = child.style.height;
398
+ if (!isPct(set)) continue;
399
+ const down =
400
+ child.style.boxSizing === 'border-box' ? 0 : child.verticalExtra;
401
+ node.setHeight(resolve(set, definite) + down);
402
+ }
403
+ }
404
+ ctx.flexDepth = depth + 1;
405
+ try {
406
+ calculate();
407
+ } finally {
408
+ ctx.flexDepth = depth;
409
+ }
410
+ }
411
+
244
412
  // Yoga aligns the lines of a box that wraps, or aligns by baselines, in
245
413
  // one pass of its own, which drops a margin (`lineMarginFix`)
246
414
  const baselines =
@@ -252,8 +420,10 @@ export function layoutFlex(
252
420
  items,
253
421
  row,
254
422
  own,
255
- box.style.flexWrap === 'wrap' || baselines,
423
+ box.style.flexWrap !== 'nowrap' || baselines,
256
424
  contentWidth,
425
+ undefined,
426
+ crossed ? crossLines(box.style, items, contentWidth, columnGap) : undefined,
257
427
  );
258
428
  // Items aligned by their baselines, which Yoga cannot see, are aligned
259
429
  // with their baselines known (`baselineLines`)
@@ -287,7 +457,7 @@ export function layoutFlex(
287
457
  items,
288
458
  row,
289
459
  own,
290
- box.style.flexWrap === 'wrap',
460
+ box.style.flexWrap !== 'nowrap',
291
461
  contentWidth,
292
462
  started,
293
463
  );
@@ -315,7 +485,7 @@ export function layoutFlex(
315
485
  items,
316
486
  row,
317
487
  own,
318
- box.style.flexWrap === 'wrap' || baselines,
488
+ box.style.flexWrap !== 'nowrap' || baselines,
319
489
  contentWidth,
320
490
  );
321
491
  }
@@ -348,6 +518,10 @@ function keepRatios(
348
518
  box.verticalExtra;
349
519
  if (Math.abs(basis - laid.main) < 0.5) continue;
350
520
  node.setFlexBasis(basis);
521
+ // and is shared out from it where its line is here (`shareOut`)
522
+ laid.basis = basis;
523
+ laid.sized = false;
524
+ laid.base = basis;
351
525
  // one held at its least size (`holdAt`) flexes from the new basis, no
352
526
  // further down than that
353
527
  if (!Number.isNaN(laid.held)) {
@@ -496,22 +670,38 @@ function placeItems(
496
670
  /** The flex box's content width: its items' containing block's. */
497
671
  contentWidth: number,
498
672
  started?: ReadonlySet<Box>,
673
+ /** Where each item is across a column that wraps (`crossLines`). */
674
+ crossed?: ReadonlyMap<Box, Across>,
499
675
  ): number {
500
676
  let bottom = 0;
677
+ // the lines of a row that wraps in reverse are turned round to run up it
678
+ const reverse = row && box.style.flexWrap === 'wrap-reverse';
501
679
  for (const { box: child, node, laid } of items) {
502
- const across = lined
503
- ? lineMarginFix(box.style, child, node, row, started?.has(child))
504
- : 0;
505
- const left = box.contentX + node.getComputedLeft() + (row ? 0 : across);
506
- const top = box.contentY + node.getComputedTop() + (row ? across : 0);
507
- const width = meant(
508
- row ? laid.main : node.getComputedWidth(),
509
- laid.set,
510
- laid.stretch,
511
- laid.width,
512
- row ? laid.held : NaN,
513
- );
514
- const itemHeight = row ? node.getComputedHeight() : laid.main;
680
+ const placed = crossed?.get(child);
681
+ const across =
682
+ lined && !placed
683
+ ? lineMarginFix(box.style, child, node, row, started?.has(child))
684
+ : 0;
685
+ const itemHeight = row ? heightOf(node, laid) : laid.main;
686
+ const left =
687
+ box.contentX +
688
+ (placed ? placed.x : node.getComputedLeft() + (row ? 0 : across));
689
+ // which Yoga turns an item round in by the height it laid it out at, and
690
+ // not the one it is (`unspace`)
691
+ const top =
692
+ box.contentY +
693
+ node.getComputedTop() +
694
+ (row ? across : 0) +
695
+ (reverse ? node.getComputedHeight() - itemHeight : 0);
696
+ const width = placed
697
+ ? placed.width
698
+ : meant(
699
+ row ? laid.main : node.getComputedWidth(),
700
+ laid.set,
701
+ laid.stretch,
702
+ laid.width,
703
+ row ? laid.held : NaN,
704
+ );
515
705
  // The item is laid out again at the width the flex pass settled on: the
516
706
  // measure function answered a question, and the answer is not a layout —
517
707
  // its line breaks were computed against a width that may have changed
@@ -590,14 +780,21 @@ function layoutItemAt(
590
780
  // size all the same (9.7), and `min-h-screen flex flex-col` around a
591
781
  // `flex-1 flex items-center` centres what is in that. At its content's
592
782
  // own height there is nothing to share, and the layout it has is that
593
- // one: the tallest card of a row is laid out the once.
783
+ // one: the tallest card of a row is laid out the once. A table shares
784
+ // what it is given past its rows among them (`layoutTable`), and a
785
+ // height short of them is none it can have: it is laid out again where
786
+ // it was given more — or less than a height of its own, which it takes
787
+ // in place of that one, down to its rows.
594
788
  const given = definite ?? (column ? height : null);
595
- const shares =
596
- box.kind === 'flex' &&
597
- given !== null &&
598
- !(
599
- Math.abs(naturalHeight(box, ctx, width, containing, laid) - given) <= 0.01
600
- );
789
+ let shares = false;
790
+ if (given !== null && (box.kind === 'flex' || box.kind === 'table')) {
791
+ const natural = naturalHeight(box, ctx, width, containing, laid);
792
+ shares =
793
+ box.kind === 'flex'
794
+ ? !(Math.abs(natural - given) <= 0.01)
795
+ : !(natural >= given - 0.01) ||
796
+ (box.style.height !== AUTO && natural > given + 0.01);
797
+ }
601
798
  if (given !== null) {
602
799
  const inner = Math.max(0, given - box.verticalExtra);
603
800
  if (Object.is(inner, percentBaseInside(box))) {
@@ -619,7 +816,7 @@ function layoutItemAt(
619
816
  const heights = definite !== null ? FLEXED_HEIGHT : USED_HEIGHT;
620
817
  heights.set(box, tall);
621
818
  try {
622
- ctx.layoutSubtree(box, width, containing);
819
+ layoutAt(box, ctx, width, containing, laid);
623
820
  } finally {
624
821
  heights.delete(box);
625
822
  }
@@ -630,8 +827,16 @@ function layoutItemAt(
630
827
  // A stretched item is taller than its content, and the box has to say so
631
828
  // or its background stops short of the row — and it is no taller than its
632
829
  // line where its content is, or than what its ratio makes of its width:
633
- // its height is the line's (CSS Flexbox 9.4, step 11).
634
- if (height > box.height || column || definite !== null) box.height = height;
830
+ // its height is the line's (CSS Flexbox 9.4, step 11). But for a table,
831
+ // whose rows are the least it can be, however short its line (CSS 2.1
832
+ // 17.5.3, as a height of its own is; Blink: "Tables can't shrink below
833
+ // their min-intrinsic size").
834
+ if (
835
+ height > box.height ||
836
+ ((column || definite !== null) && box.kind !== 'table')
837
+ ) {
838
+ box.height = height;
839
+ }
635
840
  // and a button's content is centred in the height it came to
636
841
  centreButton(box);
637
842
  moveTo(box, x, y);
@@ -653,7 +858,7 @@ function applyContainer(node: YogaNode, style: ComputedStyle): void {
653
858
  if (justify !== Y.JUSTIFY_FLEX_START) node.setJustifyContent(justify);
654
859
  const items = ALIGN[style.alignItems] ?? Y.ALIGN_STRETCH;
655
860
  if (items !== Y.ALIGN_STRETCH) node.setAlignItems(items);
656
- const lines = ALIGN[style.alignContent] ?? Y.ALIGN_STRETCH;
861
+ const lines = ALIGN[crossContent(style)] ?? Y.ALIGN_STRETCH;
657
862
  if (lines !== Y.ALIGN_FLEX_START) node.setAlignContent(lines);
658
863
  }
659
864
 
@@ -665,11 +870,16 @@ function applyItem(
665
870
  laid: Laid,
666
871
  /** Whether the flex box has a height its items are laid out in. */
667
872
  tall: boolean,
668
- /** Whether that height is definite, and a percentage may be of it: one a
669
- * column flexed the box to is not (`USED_HEIGHT`). */
670
- own: boolean,
873
+ /** That height where it is definite, and a percentage may be of it, and
874
+ * else NaN: one a column flexed the box to is not (`USED_HEIGHT`). */
875
+ definite: number,
876
+ /** Whether its line weighs it against another as it shrinks them, by
877
+ * their flex base sizes (`layoutFlex`). */
878
+ weighed: boolean,
671
879
  ): void {
672
880
  const style = box.style;
881
+ const own = Number.isFinite(definite);
882
+ const row = box.parent?.style.flexDirection.startsWith('row') ?? true;
673
883
  laid.stretch = containingWidth - box.marginLeft - box.marginRight;
674
884
  // an `auto` margin takes the free space on its side, which is how
675
885
  // `margin-left: auto` puts an item at the end of its row (CSS Flexbox
@@ -695,15 +905,34 @@ function applyItem(
695
905
  if (style.width !== AUTO) {
696
906
  laid.set = setLength(node, true, style.width, containingWidth, across);
697
907
  }
908
+ // Down a column, a basis of the content's — `content`, or a percentage
909
+ // of a height that is not definite (CSS Flexbox 7.2.3) — is its content's
910
+ // height, whatever height it has of its own (9.2.3, E), and Yoga takes
911
+ // the height of its own for a basis where it has one. So that is kept
912
+ // from Yoga, and the measure function answers with the content's.
913
+ const contentColumn =
914
+ !row && (style.flexBasis === 'content' || (isPct(style.flexBasis) && !own));
698
915
  // a percentage of a height that is not definite is `auto` (CSS 2.1 10.5),
699
- // which Yoga makes of one where the flex box has no height at all
700
- if (style.height !== AUTO && (own || !tall || !isPct(style.height))) {
701
- setLength(node, false, style.height, NaN, down);
916
+ // and is not handed to Yoga, which makes one of the height of a flex box
917
+ // with none of its own but limits on one — and the height set on its
918
+ // node, where that is a length, and whether one is
919
+ let height = NaN;
920
+ let heightSet = false;
921
+ if (
922
+ style.height !== AUTO &&
923
+ (own || !isPct(style.height)) &&
924
+ !contentColumn
925
+ ) {
926
+ height = setLength(node, false, style.height, definite, down);
927
+ heightSet = true;
702
928
  }
703
929
  // the least it may be along each axis, its border box's, as Yoga is
704
930
  // told it
705
931
  let leastWidth = NaN;
706
932
  let leastHeight = NaN;
933
+ // and the most, as Yoga is told it
934
+ let mostWidth = NaN;
935
+ let mostHeight = NaN;
707
936
  const minWidth = resolveOrNull(style.minWidth, containingWidth);
708
937
  if (minWidth !== null) {
709
938
  leastWidth = minWidth + across;
@@ -711,7 +940,10 @@ function applyItem(
711
940
  }
712
941
  if (style.maxWidth !== 'none') {
713
942
  const maxWidth = resolveOrNull(style.maxWidth, containingWidth);
714
- if (maxWidth !== null) node.setMaxWidth(maxWidth + across);
943
+ if (maxWidth !== null) {
944
+ mostWidth = maxWidth + across;
945
+ node.setMaxWidth(mostWidth);
946
+ }
715
947
  }
716
948
  if (style.widthKeyword || style.minWidthKeyword || style.maxWidthKeyword) {
717
949
  // an intrinsic size is the item's content's, measured here and handed
@@ -731,29 +963,87 @@ function applyItem(
731
963
  leastWidth = size(style.minWidthKeyword);
732
964
  node.setMinWidth(leastWidth);
733
965
  }
734
- if (style.maxWidthKeyword) node.setMaxWidth(size(style.maxWidthKeyword));
966
+ if (style.maxWidthKeyword) {
967
+ mostWidth = size(style.maxWidthKeyword);
968
+ node.setMaxWidth(mostWidth);
969
+ }
735
970
  }
736
- const minHeight = resolveOrNull(style.minHeight, NaN);
971
+ // a percentage of a definite height, which a limit on a flex item's
972
+ // height is where its flex box has one
973
+ const minHeight = resolveOrNull(style.minHeight, definite);
737
974
  if (minHeight !== null) {
738
975
  leastHeight = minHeight + down;
739
976
  node.setMinHeight(leastHeight);
740
977
  }
741
978
  if (style.maxHeight !== 'none') {
742
- const maxHeight = resolveOrNull(style.maxHeight, NaN);
743
- if (maxHeight !== null) node.setMaxHeight(maxHeight + down);
979
+ const maxHeight = resolveOrNull(style.maxHeight, definite);
980
+ if (maxHeight !== null) {
981
+ mostHeight = maxHeight + down;
982
+ node.setMaxHeight(mostHeight);
983
+ }
984
+ }
985
+
986
+ // a `border-box` maximum under the item's padding and borders is them,
987
+ // its content box none wide (CSS Sizing 3, 3.3), where Yoga lays the
988
+ // item out as wide as them and shares out its line as though it were
989
+ // as wide as the maximum: the `auto` margins after `max-width:
990
+ // 20.668px` with 23px of padding and border took the 2.33px between
991
+ if (mostWidth < box.horizontalExtra) {
992
+ mostWidth = box.horizontalExtra;
993
+ node.setMaxWidth(mostWidth);
994
+ }
995
+ if (mostHeight < box.verticalExtra) {
996
+ mostHeight = box.verticalExtra;
997
+ node.setMaxHeight(mostHeight);
998
+ }
999
+ // a maximum under the minimum is the minimum (CSS 2.1 10.4, 10.7), where
1000
+ // Yoga holds an item that comes to more than both at the maximum
1001
+ if (mostWidth < leastWidth) {
1002
+ mostWidth = leastWidth;
1003
+ node.setMaxWidth(mostWidth);
1004
+ }
1005
+ if (mostHeight < leastHeight) {
1006
+ mostHeight = leastHeight;
1007
+ node.setMaxHeight(mostHeight);
744
1008
  }
745
1009
 
746
- const row = box.parent?.style.flexDirection.startsWith('row') ?? true;
747
1010
  laid.least = row ? leastWidth : leastHeight;
1011
+ laid.most = row ? mostWidth : mostHeight;
1012
+ laid.narrowest = leastWidth;
1013
+ // A table is as wide as the flex layout makes it, as any item is: the
1014
+ // size its line flexed it to along a row (9.7), and across a column's
1015
+ // line the line's, stretched (9.4, step 11) — where in a block's flow it
1016
+ // is as wide as its columns. Laid out at that width and then shrunk to
1017
+ // them, it was a table its columns wide at the start of a column, and one
1018
+ // `flex: 1` along a row stopped at them where Chrome's fills its share
1019
+ laid.across =
1020
+ box.kind === 'table' &&
1021
+ (row ||
1022
+ (box.parent !== null &&
1023
+ !style.widthKeyword &&
1024
+ stretches(box, box.parent.style, false)));
748
1025
  // Yoga takes a length for a basis only where the flex box's main size is
749
1026
  // definite, and else the item's own size along it: down a column of no
750
1027
  // height of its own it read `flex: 0 0 3rem` as the item's height, or
751
1028
  // its content's. There the basis is handed over as that height, which is
752
1029
  // what the item's own is to a basis anyway — no more than its start
753
1030
  const setBasis = (px: number): void => {
1031
+ laid.basis = px;
754
1032
  if (row || tall) node.setFlexBasis(px);
755
1033
  else node.setHeight(px);
756
1034
  };
1035
+ // Where Yoga takes the item's own size along the main axis for its
1036
+ // basis, that size as a length, or its content's (`Laid.sized`) — and
1037
+ // its content's, whatever size it has, for a basis that says so, which
1038
+ // Yoga reads as its size down a column
1039
+ const ownSize = (content = false): void => {
1040
+ if (
1041
+ content ||
1042
+ (row ? style.width === AUTO && !style.widthKeyword : !heightSet)
1043
+ ) {
1044
+ laid.sized = true;
1045
+ } else laid.basis = row ? laid.set : height;
1046
+ };
757
1047
  // down a column, an item with a ratio and a width is as tall as that
758
1048
  // makes it, for a basis of its content or of its own `auto` height (CSS
759
1049
  // Flexbox 9.2.3, B) — where Yoga measured it, and its block layout held
@@ -769,24 +1059,36 @@ function applyItem(
769
1059
  // the content's size along a row, whatever the item's own width says
770
1060
  // (CSS Flexbox 7.2.3), which Yoga reads as `auto` and takes the width
771
1061
  // for; along a column, Yoga's measure is the content's
772
- node.setFlexBasis(contentBasis(box, ctx, containingWidth));
1062
+ laid.basis = contentBasis(box, ctx, containingWidth);
1063
+ node.setFlexBasis(laid.basis);
773
1064
  } else if (columnBasis !== null) {
774
1065
  // but a height of its own hides it from Yoga, and an item with a ratio
775
1066
  // and a width is as tall as that makes it
776
1067
  setBasis(columnBasis);
777
1068
  } else if (style.flexBasis === 'content' || style.flexBasis === AUTO) {
778
1069
  // Yoga's own
779
- } else if (isPct(style.flexBasis) && !row && tall && !own) {
1070
+ ownSize(style.flexBasis === 'content');
1071
+ } else if (isPct(style.flexBasis) && !row && !own) {
780
1072
  // a percentage of a main size that is not definite is `content` (CSS
781
1073
  // Flexbox 7.2.3): Yoga's own, down a column
1074
+ ownSize(true);
782
1075
  } else if (isPct(style.flexBasis)) {
783
- // a percentage of the main size, which is known across a row
1076
+ // a percentage of the main size, which is known across a row and down
1077
+ // a column of a definite height: of a content box's size, as a width
1078
+ // is, where the item's padding and border go on top of it
784
1079
  const basis = style.flexBasis;
785
- if (!basis.px && !basis.of) node.setFlexBasisPercent(basis.pct);
786
- else if (row && Number.isFinite(containingWidth)) {
787
- node.setFlexBasis(resolve(basis, containingWidth));
788
- } else if (basis.of) node.setFlexBasisAuto();
789
- else node.setFlexBasisPercent(basis.pct);
1080
+ const extra = row ? across : down;
1081
+ const known = row ? Number.isFinite(containingWidth) : own;
1082
+ if (known) {
1083
+ laid.basis = resolve(basis, row ? containingWidth : definite) + extra;
1084
+ }
1085
+ if (!basis.px && !basis.of && !(known && extra)) {
1086
+ node.setFlexBasisPercent(basis.pct);
1087
+ } else if (known) node.setFlexBasis(laid.basis);
1088
+ else if (basis.of) {
1089
+ node.setFlexBasisAuto();
1090
+ ownSize();
1091
+ } else node.setFlexBasisPercent(basis.pct);
790
1092
  } else setBasis(style.flexBasis + (row ? across : down));
791
1093
 
792
1094
  // The item's padding and border belong to Yoga so it can size the item,
@@ -808,8 +1110,14 @@ function applyItem(
808
1110
  // table cell's is: taken per layout of the container, a flex box in a
809
1111
  // flex box in a flex box laid its innermost out three times a level,
810
1112
  // and twelve levels took two seconds.
811
- const alongRow = box.parent?.style.flexDirection.startsWith('row') ?? true;
1113
+ const alongRow = row;
812
1114
  const content = (): number => {
1115
+ const width = maxContent();
1116
+ // its flex base size, where that is its content's (`Laid.sized`)
1117
+ if (alongRow) laid.content = width + box.horizontalExtra;
1118
+ return width;
1119
+ };
1120
+ function maxContent(): number {
813
1121
  // a replaced element's is the width it has with no limit — its
814
1122
  // natural one, or what its ratio makes of a height of its own — which
815
1123
  // its image, loading, may change, so it is not kept; taken as its
@@ -829,12 +1137,17 @@ function applyItem(
829
1137
  if (width === undefined) {
830
1138
  width = measureIntrinsicWidth(box, ctx, Infinity) - box.horizontalExtra;
831
1139
  MAX_CONTENT.set(box, width);
1140
+ // measured, it is laid out with no limit on its width, and not at
1141
+ // the one it was last laid out at, which Yoga may give it again: an
1142
+ // item `min-width: min-content` that Yoga asked at its minimum and
1143
+ // then with no limit was left laid out infinitely wide
1144
+ laid.width = NaN;
832
1145
  }
833
1146
  // within what its least and greatest heights make of widths through
834
1147
  // its ratio: its size before it is flexed, and not after
835
1148
  const extra = box.horizontalExtra;
836
1149
  return Math.max(0, transferredWidth(box, width + extra) - extra);
837
- };
1150
+ }
838
1151
  // An item with a ratio is as wide, across a column, as the height it
839
1152
  // was given makes it through the ratio, where its width is its own to
840
1153
  // find (CSS Flexbox 9.4, its hypothetical cross size from its used main
@@ -871,27 +1184,114 @@ function applyItem(
871
1184
  const flexed = box.kind === 'replaced' || !laid.auto;
872
1185
  const through =
873
1186
  widthThrough && given && !exact && flexed ? widthThrough(h) : null;
874
- // along a row, a replaced element is its own width wherever there is
875
- // room for less — its flex base size is not fitted to the room (9.2)
876
- // — and across a column it is fitted, as any item is
877
- const inner =
1187
+ // Along a row, an item's flex base size is its content at its widest,
1188
+ // whatever the room (CSS Flexbox 9.2.3 E, `content` as `max-content`),
1189
+ // which Yoga asks for as at most the row's width. One that may not
1190
+ // shrink is as wide as that (9.7): fitted to the room, a `shrink-0`
1191
+ // item of words was held to its content at its narrowest, and wrapped
1192
+ // where it overflows the row. And one that may shrink gives up room in
1193
+ // proportion to it, where its line weighs it against another (9.7,
1194
+ // step 4c, its flex shrink factor times its base): fitted, an item of
1195
+ // words wider than the row was weighed as no wider than the row, and
1196
+ // gave up too little of it beside a narrower one — 230 and 70 of a row
1197
+ // of 300, where Chrome has 245 and 55. One that shrinks alone is
1198
+ // fitted, which comes to the same and keeps the layout it is measured
1199
+ // at for the one it is given: at its widest, a paragraph in a row laid
1200
+ // itself out twice.
1201
+ // And every item of a row laid out in no room is fitted, which is the
1202
+ // flex box measured for its content at its narrowest
1203
+ // (`MIN_CONTENT_PROBE`): that is the sum of its items' content at their
1204
+ // narrowest, whatever they may shrink by, as Chrome and the css-flexbox
1205
+ // suite have it (`intrinsic-size/row-001`, `gap-015`), and not their
1206
+ // bases.
1207
+ // Across a column, the item is `fit-content` wide where it is not
1208
+ // stretched (9.4, step 7): fitted to the column, but no narrower than
1209
+ // its content at its narrowest, past the column where that is wider,
1210
+ // as a centred one is on both sides. Its `min-width` has no say in
1211
+ // that: an automatic minimum is the main axis's (4.5), so `auto` is 0
1212
+ // across a column, the same as the `min-width: 0` that lets an item
1213
+ // shrink along a row. Fitted to the column, a word wider than it ran
1214
+ // out of the item, and a centred item started at the column's start.
1215
+ // A replaced element's narrowest is its natural width, or one through
1216
+ // its ratio, and none where it has neither: an SVG with only a
1217
+ // `viewBox` is as wide as the column. And an item with something in
1218
+ // it that takes a percentage of a height is fitted to the column still:
1219
+ // the flex layout makes its height definite only after this, and
1220
+ // measured without it, an image `height: 100%` is its natural width.
1221
+ const widest =
1222
+ alongRow &&
1223
+ !exact &&
1224
+ (box.kind === 'replaced' ||
1225
+ (containingWidth !== MIN_CONTENT_PROBE &&
1226
+ (style.flexShrink === 0 || weighed)));
1227
+ const narrowest =
1228
+ alongRow || percentHeightsIn(box)
1229
+ ? undefined
1230
+ : box.kind === 'replaced'
1231
+ ? () => (replacedMinimum(box, containingWidth) ?? extra) - extra
1232
+ : () => minContentOf(box, ctx, laid) - extra;
1233
+ const fitted =
878
1234
  through !== null
879
1235
  ? through
880
- : box.kind === 'replaced' && alongRow && !exact
1236
+ : widest
881
1237
  ? content()
882
- : innerWidth(width, wm, content);
1238
+ : innerWidth(width, wm, content, narrowest);
1239
+ // Across a column, no wider than its maximum: Yoga hands the measure
1240
+ // a room within it, but holds the answer's width to it only after,
1241
+ // and keeps the height measured at the wider — an image `max-width:
1242
+ // 100%` its natural width at its narrowest was as tall as that made it
1243
+ const capped =
1244
+ !alongRow && fitted > mostWidth - extra ? mostWidth - extra : fitted;
1245
+ // And no narrower than its minimum: Yoga holds the item to that only
1246
+ // after it has the answer, which it keeps for the width that makes of
1247
+ // it, so an item measured at the room and held to a wider minimum was
1248
+ // as tall as its content wrapped at the room — `min-w-max` in a narrow
1249
+ // row, or down a column narrower than it, one line of words two lines
1250
+ // tall, and an item held to its longest word in a row that wraps
1251
+ // (`holdAt`) a line taller than that makes it. Stretched across a
1252
+ // column, it is asked at the column's width.
1253
+ const inner = Number.isNaN(laid.narrowest)
1254
+ ? capped
1255
+ : Math.max(capped, laid.narrowest - extra);
1256
+ let answer: { width: number; height: number } | undefined;
883
1257
  if (
884
1258
  box.kind === 'replaced' &&
885
1259
  exact &&
886
1260
  ratio > 0 &&
887
1261
  style.height === AUTO
888
1262
  ) {
889
- return { width: inner, height: inner / ratio };
1263
+ answer = { width: inner, height: inner / ratio };
1264
+ } else {
1265
+ answer = answers.get(inner);
1266
+ if (answer === undefined) {
1267
+ answer = measureBox(box, ctx, inner, containingWidth, laid);
1268
+ answers.set(inner, answer);
1269
+ }
890
1270
  }
891
- let answer = answers.get(inner);
892
- if (answer === undefined) {
893
- answer = measureBox(box, ctx, inner, containingWidth, laid);
894
- answers.set(inner, answer);
1271
+ // along a row, the width it answered where it might choose one
1272
+ if (alongRow && wm !== Y.MEASURE_MODE_EXACTLY) {
1273
+ laid.fitted = answer.width + box.horizontalExtra;
1274
+ }
1275
+ // down a column, its content's height where nothing held it to one:
1276
+ // its flex base size, where that is its content's (`Laid.sized`) — for
1277
+ // a basis that says so, what a height of its own has no say in
1278
+ if (!alongRow && !given) {
1279
+ if (contentColumn && style.height !== AUTO && box.kind !== 'replaced') {
1280
+ const width = inner + box.horizontalExtra;
1281
+ const kept = keptNatural(box, ctx, width, containingWidth);
1282
+ let content = kept?.content ?? NaN;
1283
+ if (Number.isNaN(content)) {
1284
+ if (kept === null || kept.serial !== box.layoutSerial) {
1285
+ layoutNaturally(box, ctx, width, containingWidth, laid);
1286
+ }
1287
+ content = columnContent(box, ctx, laid, width, containingWidth);
1288
+ }
1289
+ answer = {
1290
+ width: answer.width,
1291
+ height: Math.max(0, content - box.verticalExtra),
1292
+ };
1293
+ }
1294
+ laid.content = answer.height + box.verticalExtra;
895
1295
  }
896
1296
  return answer;
897
1297
  });
@@ -923,12 +1323,51 @@ interface Laid {
923
1323
  /** The least it may be along the main axis, where its own style says:
924
1324
  * the minimum this engine set on its node, or NaN. */
925
1325
  least: number;
1326
+ /** The most it may be along the main axis, where its own style says: the
1327
+ * maximum this engine set on its node, or NaN. */
1328
+ most: number;
1329
+ /** Its flex base size (CSS Flexbox 9.2, step 3), its border box's, where
1330
+ * its style says what that is — a basis or a size along the main axis
1331
+ * that is a length — or NaN where its content does (`content`) or Yoga
1332
+ * resolves it. */
1333
+ basis: number;
1334
+ /** Whether its flex base size is its content's size along the main axis,
1335
+ * which the measure function notes (`content`). */
1336
+ sized: boolean;
1337
+ /** Its content's size along the main axis, its border box's, as the
1338
+ * measure function last found it with nothing holding it to a size
1339
+ * there: its max-content width along a row, its height at its width
1340
+ * down a column. NaN until measured so. */
1341
+ content: number;
1342
+ /** The border-box width the measure function last answered along a row
1343
+ * where Yoga asked for one it might choose, its content's fitted to the
1344
+ * room: the flex base size Yoga takes for it (`FlexLine.probe`). */
1345
+ fitted: number;
1346
+ /** The flex base size its line was shared out from where this engine
1347
+ * shared it (`resolveLine`), or NaN. */
1348
+ base: number;
1349
+ /** The least border-box width its node was given, along a row or across
1350
+ * a column: its own minimum, or along a row the automatic one it is
1351
+ * held to (`holdAt`), or NaN. It is measured at no narrower. */
1352
+ narrowest: number;
926
1353
  /** The size along the main axis it is frozen at, inflexible (`freezeAt`),
927
1354
  * or NaN. */
928
1355
  held: number;
1356
+ /** Its automatic minimum along the main axis (CSS Flexbox 4.5), where it
1357
+ * is held to it (`holdAt`), or NaN. */
1358
+ minimum: number;
929
1359
  /** Whether that is its automatic minimum (`holdAt`), its content's size,
930
- * and not a minimum the line stopped at (`stoppedLine`). */
1360
+ * and not a size its line was shared out to (`resolveLine`). */
931
1361
  auto: boolean;
1362
+ /** Whether it is a table the flex layout gives its width — flexed along a
1363
+ * row, stretched across a column's line — which it is laid out at as its
1364
+ * own (`STRETCHED_ACROSS`, `layoutAt`). */
1365
+ across: boolean;
1366
+ /** Its border box's height as Yoga sized it before it laid it out again
1367
+ * across its line, at another: down a column that wraps (`unstretch`),
1368
+ * or across a row whose lines are spaced out (`unspace`). NaN where that
1369
+ * is the height Yoga has for it. */
1370
+ unstretched: number;
932
1371
  }
933
1372
 
934
1373
  /**
@@ -1067,7 +1506,7 @@ function layoutNaturally(
1067
1506
  box.height = kept.height;
1068
1507
  } else {
1069
1508
  const at = ctx.positioned.length;
1070
- ctx.layoutSubtree(box, width, containing);
1509
+ layoutAt(box, ctx, width, containing, laid);
1071
1510
  // a replaced box is sized, not laid out, and has no layout to count
1072
1511
  if (box.kind !== 'replaced') {
1073
1512
  NATURAL.set(box, {
@@ -1090,6 +1529,33 @@ function layoutNaturally(
1090
1529
  laid.tall = NaN;
1091
1530
  }
1092
1531
 
1532
+ /**
1533
+ * Lay an item out at a border-box width: a table the flex layout gives its
1534
+ * width at that width as its own (`Laid.across`), for this layout alone. A
1535
+ * width Yoga asks its content at is one the table comes to by its columns
1536
+ * all the same — its widest, or the room where that is less — so that is
1537
+ * no matter; an unbounded one, a probe of a box around the flex box, is
1538
+ * the table's to shrink in.
1539
+ */
1540
+ function layoutAt(
1541
+ box: Box,
1542
+ ctx: LayoutContext,
1543
+ width: number,
1544
+ containing: number,
1545
+ laid: Laid,
1546
+ ): void {
1547
+ if (!laid.across || !Number.isFinite(width)) {
1548
+ ctx.layoutSubtree(box, width, containing);
1549
+ return;
1550
+ }
1551
+ STRETCHED_ACROSS.add(box);
1552
+ try {
1553
+ ctx.layoutSubtree(box, width, containing);
1554
+ } finally {
1555
+ STRETCHED_ACROSS.delete(box);
1556
+ }
1557
+ }
1558
+
1093
1559
  /**
1094
1560
  * The layout this pass made of an item at its content's own height
1095
1561
  * (`layoutNaturally`), where one at `width` now would be the same: the same
@@ -1233,11 +1699,13 @@ function readsPercentBase(box: Box): boolean {
1233
1699
 
1234
1700
  /** The width an item's content is measured at: the width Yoga gives it
1235
1701
  * exactly, or its max-content width, no wider than any it may take up to
1236
- * (CSS Flexbox 9.2). */
1702
+ * (CSS Flexbox 9.2) — and, given its min-content width, no narrower than
1703
+ * that either. */
1237
1704
  function innerWidth(
1238
1705
  width: number,
1239
1706
  mode: number,
1240
1707
  maxContent: () => number,
1708
+ minContent?: () => number,
1241
1709
  ): number {
1242
1710
  if (mode === Y.MEASURE_MODE_EXACTLY && Number.isFinite(width)) {
1243
1711
  return Math.max(0, width);
@@ -1247,9 +1715,9 @@ function innerWidth(
1247
1715
  return content;
1248
1716
  }
1249
1717
  // a room exactly as wide as the content, as Yoga holds it, is room for it
1250
- return content <= width || nearly(content, width)
1251
- ? content
1252
- : Math.max(0, width);
1718
+ if (content <= width || nearly(content, width)) return content;
1719
+ const room = Math.max(0, width);
1720
+ return minContent ? Math.min(content, Math.max(room, minContent())) : room;
1253
1721
  }
1254
1722
 
1255
1723
  /**
@@ -1277,12 +1745,6 @@ function autoMinimums(
1277
1745
  for (const { box, node, laid } of items) {
1278
1746
  if (box.kind === 'text' || box.kind === 'break') continue;
1279
1747
  const style = box.style;
1280
- // a box that scrolls or clips has none, but for one that asks for its
1281
- // content's height
1282
- const asked = !row && style.minHeightKeyword !== null;
1283
- if (!asked && scrolls(style)) {
1284
- continue;
1285
- }
1286
1748
  const width = meant(
1287
1749
  row ? laid.main : node.getComputedWidth(),
1288
1750
  laid.set,
@@ -1290,6 +1752,39 @@ function autoMinimums(
1290
1752
  laid.width,
1291
1753
  row ? laid.held : NaN,
1292
1754
  );
1755
+ // A table is no smaller than it can be along either axis, whatever its
1756
+ // own least says, `min-width: 0` and `min-height: 0` among it: its
1757
+ // columns at their narrowest along a row, and down a column its rows
1758
+ // and captions at the width it has, however short a height of its own
1759
+ // is (CSS 2.1 17.5.2 and 17.5.3; Blink, `length_utils.cc`: "Tables
1760
+ // can't shrink below their min-intrinsic size"). The line is laid out
1761
+ // with it so, and the items after it start where it ends: Yoga had it
1762
+ // as small as its line, and the next item over the rest of it — read
1763
+ // as a block's content, two pixels into it, the spacing under its rows
1764
+ if (box.kind === 'table') {
1765
+ // as wide as its columns at their widest, where Yoga measured that,
1766
+ // it has room for them at their narrowest, which is a layout less
1767
+ const widest = row ? MAX_CONTENT.get(box) : undefined;
1768
+ if (
1769
+ widest !== undefined &&
1770
+ laid.main >= widest + box.horizontalExtra - 0.01
1771
+ ) {
1772
+ continue;
1773
+ }
1774
+ const least = row
1775
+ ? minContentOf(box, ctx, laid)
1776
+ : tableContent(box, ctx, width, containingWidth, laid);
1777
+ if (laid.main >= least - 0.01) continue;
1778
+ holdAt(node, laid, row, least, hold);
1779
+ changed = true;
1780
+ continue;
1781
+ }
1782
+ // a box that scrolls or clips has none, but for one that asks for its
1783
+ // content's height
1784
+ const asked = !row && style.minHeightKeyword !== null;
1785
+ if (!asked && scrolls(style)) {
1786
+ continue;
1787
+ }
1293
1788
  // a replaced item's content along a row is its natural width
1294
1789
  if (row && box.kind === 'replaced') {
1295
1790
  if (style.minWidth !== AUTO || style.minWidthKeyword) continue;
@@ -1410,26 +1905,9 @@ function autoMinimums(
1410
1905
  } else {
1411
1906
  // its height, or where it has one of its own and its content comes to
1412
1907
  // less, its content's: the lesser of the two (4.5)
1413
- let content = read;
1414
- if (!Number.isNaN(content)) {
1415
- // read before
1416
- } else if (box.kind === 'replaced') content = laid.height;
1417
- else if (style.height !== AUTO && percentHeightsIn(box)) {
1418
- // what its content comes to where it has no height to take
1419
- // percentages of, as an intrinsic size is measured: laid out so
1420
- // apart, and the final pass lays it out again
1421
- FLEXED_HEIGHT.set(box, NaN);
1422
- try {
1423
- ctx.layoutSubtree(box, width, containingWidth);
1424
- } finally {
1425
- FLEXED_HEIGHT.delete(box);
1426
- }
1427
- content = ratioContent(box, contentBottom(box) + box.verticalExtra);
1428
- laid.width = NaN;
1429
- } else {
1430
- content = ratioContent(box, contentBottom(box) + box.verticalExtra);
1431
- remember(box, ctx, width, containingWidth, 'content', content);
1432
- }
1908
+ let content = Number.isNaN(read)
1909
+ ? columnContent(box, ctx, laid, width, containingWidth)
1910
+ : read;
1433
1911
  // within what the ratio makes of its least and greatest widths
1434
1912
  if (box.kind !== 'replaced') {
1435
1913
  content = transferredHeight(box, content, containingWidth);
@@ -1448,6 +1926,58 @@ function autoMinimums(
1448
1926
  return changed;
1449
1927
  }
1450
1928
 
1929
+ /**
1930
+ * How far down a column item's content comes at a border-box width, its
1931
+ * border box's, from the layout its content's own height makes of it
1932
+ * there (`layoutNaturally`), which it has: what a height of its own has no
1933
+ * say in, which its automatic minimum reads (CSS Flexbox 4.5), and a flex
1934
+ * basis of `content` (9.2.3, E).
1935
+ */
1936
+ function columnContent(
1937
+ box: Box,
1938
+ ctx: LayoutContext,
1939
+ laid: Laid,
1940
+ width: number,
1941
+ containingWidth: number,
1942
+ ): number {
1943
+ if (box.kind === 'replaced') return laid.height;
1944
+ if (box.style.height !== AUTO && percentHeightsIn(box)) {
1945
+ // what its content comes to where it has no height to take
1946
+ // percentages of, as an intrinsic size is measured: laid out so
1947
+ // apart, and the final pass lays it out again
1948
+ FLEXED_HEIGHT.set(box, NaN);
1949
+ try {
1950
+ ctx.layoutSubtree(box, width, containingWidth);
1951
+ } finally {
1952
+ FLEXED_HEIGHT.delete(box);
1953
+ }
1954
+ laid.width = NaN;
1955
+ return ratioContent(box, contentBottom(box) + box.verticalExtra);
1956
+ }
1957
+ const content = ratioContent(box, contentBottom(box) + box.verticalExtra);
1958
+ remember(box, ctx, width, containingWidth, 'content', content);
1959
+ return content;
1960
+ }
1961
+
1962
+ /** A table item's rows and captions at a border-box width, its border box's
1963
+ * height apart from one it sets (`TABLE_CONTENT`): read from its layout
1964
+ * there, which it is given where it has another, and kept for the pass. */
1965
+ function tableContent(
1966
+ box: Box,
1967
+ ctx: LayoutContext,
1968
+ width: number,
1969
+ containing: number,
1970
+ laid: Laid,
1971
+ ): number {
1972
+ const kept = keptNatural(box, ctx, width, containing);
1973
+ if (kept !== null && !Number.isNaN(kept.content)) return kept.content;
1974
+ // where the box has that layout still, it is put back and not made again
1975
+ layoutNaturally(box, ctx, width, containing, laid);
1976
+ const content = TABLE_CONTENT.get(box) ?? box.height;
1977
+ remember(box, ctx, width, containing, 'content', content);
1978
+ return content;
1979
+ }
1980
+
1451
1981
  /** A laid-out item's content height, its border box's: no less than its
1452
1982
  * width through its ratio, height of its own or not (CSS Sizing 4, 5.1). */
1453
1983
  function ratioContent(box: Box, content: number): number {
@@ -1504,10 +2034,13 @@ function holdAt(
1504
2034
  least: number,
1505
2035
  hold: Hold,
1506
2036
  ): void {
1507
- if (row) node.setMinWidth(least);
1508
- else node.setMinHeight(least);
2037
+ if (row) {
2038
+ node.setMinWidth(least);
2039
+ laid.narrowest = least;
2040
+ } else node.setMinHeight(least);
1509
2041
  if (!hold.frozen) return;
1510
2042
  laid.auto = true;
2043
+ laid.minimum = least;
1511
2044
  freezeAt(node, laid, row, least, hold);
1512
2045
  }
1513
2046
 
@@ -1528,134 +2061,725 @@ function freezeAt(
1528
2061
  else node.setHeight(size);
1529
2062
  }
1530
2063
 
2064
+ /** The one line of a flex box that does not wrap, as it is shared out here
2065
+ * (`resolveLine`). */
2066
+ interface FlexLine {
2067
+ /** Whether its items are frozen at the sizes 9.7 shares it out to. */
2068
+ resolved: boolean;
2069
+ /** Its size along the main axis, where the box has one: its content
2070
+ * box's width along a row, its height down a column of a height. NaN
2071
+ * down a column that is as tall as its items make it, within limits. */
2072
+ size: number;
2073
+ /** Those limits, the least and the most its content box may be tall. */
2074
+ floor: number;
2075
+ ceiling: number;
2076
+ /** What is between the items' border boxes along it: the gaps, and
2077
+ * their margins, an `auto` one none (8.1). */
2078
+ between: number;
2079
+ /**
2080
+ * Whether it is a row laid out in no room at all — how the box's
2081
+ * min-content width is measured (`MIN_CONTENT_PROBE`), where what an
2082
+ * item contributes to that is its min-content width and not a share of
2083
+ * a line. An item whose flex base size is its content's is taken there
2084
+ * at the size Yoga took, its content fitted to no room, and not at its
2085
+ * max-content width (`Laid.fitted`): a `flex: none` box of two floats
2086
+ * counted at its widest made a `width: min-content` flex box half as
2087
+ * wide again as Chrome has it. A row that is truly as wide as nothing is
2088
+ * taken the same way, and is laid out as Yoga has it.
2089
+ */
2090
+ probe: boolean;
2091
+ }
2092
+
2093
+ /** What is between the border boxes of a line's items along the main
2094
+ * axis: the gaps, and their margins, an `auto` one none — which is what
2095
+ * it is while the line is shared out (CSS Flexbox 9.7, 8.1). */
2096
+ function betweenOf(
2097
+ items: readonly { box: Box }[],
2098
+ row: boolean,
2099
+ gap: number,
2100
+ ): number {
2101
+ let between = gap * Math.max(0, items.length - 1);
2102
+ for (const { box } of items) {
2103
+ const style = box.style;
2104
+ if (row) {
2105
+ if (style.marginLeft !== AUTO) between += box.marginLeft;
2106
+ if (style.marginRight !== AUTO) between += box.marginRight;
2107
+ } else {
2108
+ if (style.marginTop !== AUTO) between += box.marginTop;
2109
+ if (style.marginBottom !== AUTO) between += box.marginBottom;
2110
+ }
2111
+ }
2112
+ return between;
2113
+ }
2114
+
1531
2115
  /**
1532
- * Put right the one line of a box that does not wrap, where every item of
1533
- * it that may shrink is stopped by its minimum and Yoga made them wider for
1534
- * it; whether Yoga was asked again. The line is laid out with each of them
1535
- * frozen at its minimum, which is where CSS Flexbox leaves them (9.7, step
1536
- * 4): the line is short of room with every one of them as small as it
1537
- * goes.
2116
+ * Share out the one line of a box that does not wrap as CSS Flexbox 9.7
2117
+ * does, where Yoga's answer for it may not be that, and freeze every item
2118
+ * at the size it comes to; whether Yoga was asked again.
2119
+ *
2120
+ * Yoga 3.2.1 shares a line out in two passes where 9.7 goes round until
2121
+ * no item's limit stops it, and each pass has its own way of coming out
2122
+ * elsewhere:
1538
2123
  *
1539
- * Yoga shares what a line is short of in two passes. The first takes each
1540
- * item a minimum stops out of the sum of the scaled shrink factors, one
1541
- * subtraction an item, and the second divides what the line is still short
1542
- * of by what is left of the sum. With every item stopped that is nothing:
1543
- * `(a + b) - a - b`, in float32s, which is 0 for some sizes and a rounding
1544
- * either side of it for others, 2^-18 for sizes about a hundred. Yoga
1545
- * 3.2.1 tests for 0, and the guard it has had since, a sum under 1e-6, is
1546
- * under that rounding (react/yoga#1665, #1974). A negative rounding sends
1547
- * every item under its minimum, which stops it, and a positive one makes
1548
- * each of them wider by billions of pixels: `width: 110.992px; min-width:
1549
- * 110.992px` beside `62.705625px` of the same, in a row 50 wide, were
1550
- * 3599091712 and 2033329408 wide, where 100 and 60 were as they are. So
1551
- * were two items with no minimum but their padding, and two with
1552
- * `min-width: 0` beside one that does not shrink and is wider than the row
1553
- * alone.
2124
+ * - the first takes each item a limit stops out of the sum of the flex
2125
+ * factors as it goes, and divides what the line is short of by that
2126
+ * running sum, but takes the item's size off what the line is short of
2127
+ * only once it has been round them all (react/yoga#2006, put right on
2128
+ * Yoga's `main` behind an errata bit and in no release). Every item
2129
+ * after the first one stopped is shrunk too far for it, and may be
2130
+ * stopped too: `width: 100px` three times with `min-width` 95px, 45px
2131
+ * and 0 in a row of 150 was 100, 100 and 100 wide, and overflowed it,
2132
+ * where 9.7 has 95, 45 and 10 — with every item stopped, the line was
2133
+ * left no shorter than it started, and the second pass shared out
2134
+ * nothing. Where what is left of the sum is a float's rounding of
2135
+ * nothing, the second pass divided by that, and each item came out
2136
+ * billions of pixels wide, or none;
2137
+ * - it weighs an item's share of a shortfall by its flex base size where
2138
+ * the item is stopped by a limit before it is shrunk, and adds it to the
2139
+ * sum at its size within the limit, or the other way round: `width:
2140
+ * 200px; max-width: 110.992px` beside `200px` within `62.706px`, each
2141
+ * with a `min-width`, in a row of 50 were 76.67 and 43.31, where 9.7
2142
+ * shrinks both from 200 and has them at their minimums;
2143
+ * - and with no limit at work, it weighs that share by the flex base size,
2144
+ * its border box's, where 9.7 weighs it by the inner flex base size, its
2145
+ * content box's (step 4c), so an item gives up more the more padding
2146
+ * and borders it has (`weighsPadding`);
2147
+ * - a limit that is more than the flex base size stops the item before
2148
+ * any sharing out, and Yoga shares out from there: `flex: 1;
2149
+ * min-width: 200px` beside a `flex: 1` in a row of 600 was 400 and
2150
+ * 200, where each grows from 0 and is 300;
2151
+ * - where the shrink factors come to less than 1, the line keeps that
2152
+ * part of what it is short of (9.7, step 4b), and Yoga shares all of it
2153
+ * out: a `flex-shrink: 0.5` alone was as wide as its row, where it
2154
+ * keeps half of what it overflows by.
1554
2155
  *
1555
- * A minimum of the item's own is Yoga's to hold it to, so this looks at
1556
- * its answer, for a line that is two things at once. It is short of room
1557
- * with every item that may shrink at its minimum — where 9.7 leaves each
1558
- * of them at that, whatever their sizes. And one of them is wider than its
1559
- * hypothetical size (9.7, step 2), which no sharing out of what a line is
1560
- * short of makes an item: Yoga is asked for the line with nothing
1561
- * shrinking, where each item is that size. Any other line is put back as
1562
- * it was, the two layouts that took being the cost of one that is short of
1563
- * room at its minimums and was not shared out to them; a line with room
1564
- * for its minimums, or one whose every item sits at its own, has cost the
1565
- * sum of its sizes.
2156
+ * Which is why a line is looked at only where Yoga's answer shows one of
2157
+ * these could be at work (`unshared`), and else left as it is: a line
2158
+ * none of whose items is stopped, that has been shared out, whose items'
2159
+ * limits do not clamp their flex base sizes, whose shrink factors come to
2160
+ * 1 or more, and which is not short of room with padding and borders more
2161
+ * of one item's flex base size than of another's is shared out by Yoga as
2162
+ * by 9.7.
1566
2163
  *
1567
- * An automatic minimum is no part of it: an item held at one is frozen
1568
- * already (`holdAt`), and one not yet held has no minimum to Yoga but its
1569
- * padding and border, where this leaves it for `autoMinimums` to find
1570
- * short of its content.
2164
+ * Each item's flex base size is what its style says it is, or its
2165
+ * content's size as the measure function found it (`Laid.sized`), and
2166
+ * where neither is known, Yoga is asked for the line with no limits and
2167
+ * nothing flexing, where each item is that size (`askBases`). An item held
2168
+ * at its automatic minimum is frozen there already (`holdAt`), and the
2169
+ * rest are shared out around it.
1571
2170
  */
1572
- function stoppedLine(
2171
+ function resolveLine(
1573
2172
  items: readonly { box: Box; node: YogaNode; laid: Laid }[],
1574
2173
  row: boolean,
1575
- /** The flex box's size along the main axis. */
2174
+ line: FlexLine,
2175
+ /** The size along the main axis Yoga laid the line out in. */
1576
2176
  room: number,
1577
- /** The gap between two items. */
1578
- gap: number,
1579
2177
  hold: Hold,
1580
2178
  /** Lay the flex box out, as its nodes now are. */
1581
2179
  ask: () => void,
1582
2180
  ): boolean {
1583
- // what is between the items' border boxes: the gaps, and their margins
1584
- let between = gap * (items.length - 1);
1585
- // the line's size with each item that may shrink at its minimum
1586
- let least = 0;
2181
+ if (!unshared(items, row, room - line.between, line.probe)) return false;
2182
+ let unknown = false;
2183
+ for (const { box, laid } of items) {
2184
+ laid.base = baseOf(box, laid, row, line.probe);
2185
+ if (Number.isNaN(laid.base) && Number.isNaN(laid.held)) unknown = true;
2186
+ }
2187
+ if (unknown) askBases(items, row, ask);
2188
+ const sizes = shareLine(items, row, line);
2189
+ if (items.every(({ laid }, i) => Math.abs(sizes[i] - laid.main) <= NEAR)) {
2190
+ // Yoga's answer, which asking for the bases has to be put back to
2191
+ if (unknown) ask();
2192
+ return unknown;
2193
+ }
2194
+ line.resolved = true;
2195
+ freezeLine(items, row, sizes, hold);
2196
+ ask();
2197
+ return true;
2198
+ }
2199
+
2200
+ /** Freeze each item of a line at the size it was shared out to, but for
2201
+ * one held at its automatic minimum whose flex base size is not known,
2202
+ * which is frozen there already. */
2203
+ function freezeLine(
2204
+ items: readonly { node: YogaNode; laid: Laid }[],
2205
+ row: boolean,
2206
+ sizes: readonly number[],
2207
+ hold: Hold,
2208
+ ): void {
2209
+ items.forEach(({ node, laid }, i) => {
2210
+ if (Number.isNaN(laid.base)) return;
2211
+ freezeAt(node, laid, row, sizes[i], hold);
2212
+ // its content's size, where that is what it came to (`Laid.auto`)
2213
+ laid.auto = sizes[i] <= laid.minimum + NEAR;
2214
+ });
2215
+ }
2216
+
2217
+ /** How near to its room a line may be and have room to spare
2218
+ * (`cappedLines`). */
2219
+ const SHORT = 0.001;
2220
+
2221
+ /** Share a line this engine has shared out again from its items' flex base
2222
+ * sizes, and freeze each item at its size: where one is since held at its
2223
+ * content's size, the rest of the line is shared out around it. */
2224
+ function shareOut(
2225
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
2226
+ row: boolean,
2227
+ line: FlexLine,
2228
+ hold: Hold,
2229
+ ): void {
2230
+ const sizes = shareLine(items, row, line);
2231
+ freezeLine(items, row, sizes, hold);
2232
+ }
2233
+
2234
+ /** Each item's size along the main axis as 9.7 shares the line out from
2235
+ * their flex base sizes (`Laid.base`); one held at its automatic minimum
2236
+ * is frozen there. */
2237
+ function shareLine(
2238
+ items: readonly { box: Box; laid: Laid }[],
2239
+ row: boolean,
2240
+ line: FlexLine,
2241
+ ): number[] {
2242
+ const flexing: Flexing[] = items.map(({ box, laid }) => {
2243
+ // an automatic minimum it is held to is its minimum, which 9.7 freezes
2244
+ // it at where the line shares out less to it — and not before, which
2245
+ // would leave it out of the free space the line started with (4b)
2246
+ let least = leastOf(box, laid, row);
2247
+ if (!Number.isNaN(laid.minimum)) least = Math.max(least, laid.minimum);
2248
+ return {
2249
+ base: laid.base,
2250
+ least,
2251
+ most: Math.max(mostOf(box, laid, row), least),
2252
+ extra: row ? box.horizontalExtra : box.verticalExtra,
2253
+ grow: box.style.flexGrow,
2254
+ shrink: box.style.flexShrink,
2255
+ frozen: Number.isNaN(laid.base) ? laid.held : NaN,
2256
+ };
2257
+ });
2258
+ let room = line.size;
2259
+ if (Number.isNaN(room)) {
2260
+ // down a column that is as tall as its items, within its limits: the
2261
+ // sum of their hypothetical sizes (9.9.1)
2262
+ room = line.between;
2263
+ for (const item of flexing) room += hypotheticalOf(item);
2264
+ room = Math.max(line.floor, Math.min(line.ceiling, room));
2265
+ }
2266
+ return resolveLengths(flexing, room - line.between);
2267
+ }
2268
+
2269
+ /**
2270
+ * Whether Yoga's answer for the one line of a box that does not wrap may
2271
+ * not be what CSS Flexbox 9.7 makes of it (`resolveLine`): where an item
2272
+ * has a limit of its own along the main axis, which 9.7 is then asked —
2273
+ * sums of numbers, where each item's flex base size is known — and where
2274
+ * none has, what shows in Yoga's answer: an item a limit stopped as the
2275
+ * line was shared out, which with none of its own is its padding and
2276
+ * borders, a line not shared out while an item may yet flex the way it is
2277
+ * out, or flex factors that come to less than 1, which Yoga shares out as
2278
+ * though they came to 1 — or, for one item that may grow and shrink, from
2279
+ * a flex basis of nothing.
2280
+ */
2281
+ function unshared(
2282
+ items: readonly { box: Box; laid: Laid }[],
2283
+ row: boolean,
2284
+ /** The room for the items' border boxes. */
2285
+ room: number,
2286
+ /** Whether the line is a min-content probe's (`FlexLine.probe`). */
2287
+ probe: boolean,
2288
+ ): boolean {
2289
+ let sum = 0;
2290
+ for (const { laid } of items) sum += laid.main;
2291
+ const over = sum > room + NEAR;
2292
+ const under = sum < room - NEAR;
2293
+ let grows = 0;
2294
+ let shrinks = 0;
2295
+ let growable = false;
2296
+ for (const { box, laid } of items) {
2297
+ // held at its content's size, and frozen (`holdAt`)
2298
+ if (!Number.isNaN(laid.held)) continue;
2299
+ if (!Number.isNaN(laid.least) || !Number.isNaN(laid.most)) return true;
2300
+ const { flexGrow: grow, flexShrink: shrink } = box.style;
2301
+ const least = leastOf(box, laid, row);
2302
+ const base = baseOf(box, laid, row, probe);
2303
+ const size = laid.main;
2304
+ grows += grow;
2305
+ shrinks += shrink;
2306
+ // stopped by its padding where it was shrunk to it, and not where it
2307
+ // is that size unflexed
2308
+ if (
2309
+ shrink > 0 &&
2310
+ !under &&
2311
+ size <= least + NEAR &&
2312
+ !(base <= size + NEAR)
2313
+ ) {
2314
+ return true;
2315
+ }
2316
+ if (over && shrink > 0 && size > least + NEAR) return true;
2317
+ if (under && grow > 0) growable = true;
2318
+ }
2319
+ return (
2320
+ (growable && grows >= 1) ||
2321
+ (!under && shrinks > 0 && shrinks < 1) ||
2322
+ (!over && grows > 0 && grows < 1) ||
2323
+ (!under && weighsPadding(items, row, room, probe))
2324
+ );
2325
+ }
2326
+
2327
+ /**
2328
+ * Whether Yoga may have shared out what a line is short of by its items'
2329
+ * padding and borders, where 9.7 leaves them out: Yoga weighs an item's
2330
+ * share by its flex shrink factor times its flex base size, its border
2331
+ * box's, and 9.7 by the factor times its inner flex base size, its content
2332
+ * box's (step 4c). The two are the same shares where padding and borders
2333
+ * are the same part of the flex base size of every item that shrinks —
2334
+ * none of it, most often — and else Yoga takes more from an item the more
2335
+ * of them it has: `width: 100px; padding-left: 50px` beside `width: 100px`
2336
+ * in a row of 150 was 90 and 60 wide, where each gives up 50 of its
2337
+ * content and is 100 and 50. Where an item's flex base size is not known,
2338
+ * its padding may be any part of it, and the line is looked at.
2339
+ */
2340
+ function weighsPadding(
2341
+ items: readonly { box: Box; laid: Laid }[],
2342
+ row: boolean,
2343
+ /** The room for the items' border boxes. */
2344
+ room: number,
2345
+ probe: boolean,
2346
+ ): boolean {
2347
+ let sum = 0;
1587
2348
  let shrinking = 0;
1588
- let over = false;
2349
+ let padded = false;
2350
+ let unknown = false;
2351
+ // the part of the first flex base size that shrinks that is padding and
2352
+ // borders, and whether that of another is not the same
2353
+ let part = NaN;
2354
+ let uneven = false;
1589
2355
  for (const { box, laid } of items) {
1590
- const style = box.style;
1591
- // an `auto` margin is none on a line with no room (8.1)
2356
+ if (!Number.isNaN(laid.held)) {
2357
+ sum += laid.held;
2358
+ continue;
2359
+ }
2360
+ const base = baseOf(box, laid, row, probe);
2361
+ if (Number.isNaN(base)) unknown = true;
2362
+ else sum += base;
2363
+ if (!(box.style.flexShrink > 0)) continue;
2364
+ shrinking += 1;
2365
+ const extra = row ? box.horizontalExtra : box.verticalExtra;
2366
+ if (extra > 0) padded = true;
2367
+ if (Number.isNaN(base)) continue;
2368
+ const of = base > 0 ? extra / base : 0;
2369
+ if (Number.isNaN(part)) part = of;
2370
+ else if (Math.abs(of - part) > 1e-9) uneven = true;
2371
+ }
2372
+ if (shrinking < 2 || !padded) return false;
2373
+ return unknown || (uneven && sum > room + NEAR);
2374
+ }
2375
+
2376
+ /** How near two sizes along the main axis are the same, as Yoga's float32s
2377
+ * have them. */
2378
+ const NEAR = 0.01;
2379
+
2380
+ /** An item's flex base size, its border box's, where this engine knows it:
2381
+ * what its style says, or its content's size as last measured — fitted
2382
+ * to no room, in a min-content probe (`FlexLine.probe`) — and no less than
2383
+ * its padding and borders, as Yoga has it. Else NaN. */
2384
+ function baseOf(box: Box, laid: Laid, row: boolean, probe: boolean): number {
2385
+ const extra = row ? box.horizontalExtra : box.verticalExtra;
2386
+ const content = probe ? laid.fitted : laid.content;
2387
+ return Math.max(laid.sized ? content : laid.basis, extra);
2388
+ }
2389
+
2390
+ /**
2391
+ * Give each item that is not frozen the flex base size Yoga makes of it,
2392
+ * where this engine does not know it (`baseOf`): the line laid out with no
2393
+ * limit on any of them along the main axis and none of them flexing, where
2394
+ * each is that size. Their limits and flex factors are put back after.
2395
+ */
2396
+ function askBases(
2397
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
2398
+ row: boolean,
2399
+ ask: () => void,
2400
+ ): void {
2401
+ const open = items.filter(({ laid }) => Number.isNaN(laid.held));
2402
+ for (const { node } of open) {
1592
2403
  if (row) {
1593
- if (style.marginLeft !== AUTO) between += box.marginLeft;
1594
- if (style.marginRight !== AUTO) between += box.marginRight;
2404
+ node.setMinWidth(Number.NaN);
2405
+ node.setMaxWidth(Number.NaN);
1595
2406
  } else {
1596
- if (style.marginTop !== AUTO) between += box.marginTop;
1597
- if (style.marginBottom !== AUTO) between += box.marginBottom;
2407
+ node.setMinHeight(Number.NaN);
2408
+ node.setMaxHeight(Number.NaN);
2409
+ }
2410
+ node.setFlexGrow(0);
2411
+ node.setFlexShrink(0);
2412
+ }
2413
+ ask();
2414
+ for (const { box, node, laid } of open) {
2415
+ if (Number.isNaN(laid.base)) {
2416
+ laid.base = row ? node.getComputedWidth() : node.getComputedHeight();
2417
+ }
2418
+ if (!Number.isNaN(laid.least)) {
2419
+ if (row) node.setMinWidth(laid.least);
2420
+ else node.setMinHeight(laid.least);
2421
+ }
2422
+ if (!Number.isNaN(laid.most)) {
2423
+ if (row) node.setMaxWidth(laid.most);
2424
+ else node.setMaxHeight(laid.most);
2425
+ }
2426
+ node.setFlexGrow(box.style.flexGrow);
2427
+ node.setFlexShrink(box.style.flexShrink);
2428
+ }
2429
+ }
2430
+
2431
+ /** An item as its line is shared out (`resolveLengths`), each size its
2432
+ * border box's along the main axis. */
2433
+ interface Flexing {
2434
+ base: number;
2435
+ /** Its least size, no less than `extra`, and its most, no less than
2436
+ * that. */
2437
+ least: number;
2438
+ most: number;
2439
+ /** Its padding and borders. */
2440
+ extra: number;
2441
+ grow: number;
2442
+ shrink: number;
2443
+ /** The size it is frozen at from the start, or NaN. */
2444
+ frozen: number;
2445
+ }
2446
+
2447
+ /** An item's hypothetical main size: its flex base size within its limits
2448
+ * (CSS Flexbox 9.2, step 3), or where it is frozen, that size. */
2449
+ function hypotheticalOf(item: Flexing): number {
2450
+ return Number.isNaN(item.frozen)
2451
+ ? Math.max(item.least, Math.min(item.most, item.base))
2452
+ : item.frozen;
2453
+ }
2454
+
2455
+ /**
2456
+ * Resolve the flexible lengths of a line (CSS Flexbox 9.7): each item's
2457
+ * size along the main axis, sharing out the room its border boxes have
2458
+ * from their flex base sizes, and going round again with each item a limit
2459
+ * stops frozen at it until none is — the minimums where the line is short
2460
+ * of more than the maximums give back, and the other way round.
2461
+ */
2462
+ function resolveLengths(line: readonly Flexing[], room: number): number[] {
2463
+ const count = line.length;
2464
+ const size = line.map(hypotheticalOf);
2465
+ let sum = 0;
2466
+ for (const s of size) sum += s;
2467
+ // step 1: grow where the line has room at the items' hypothetical sizes
2468
+ const grow = sum < room;
2469
+ // step 2: an item that does not flex, or would flex away from its limit,
2470
+ // is frozen at its hypothetical size — a flex base size Yoga worked out
2471
+ // being as near to that as its float32s come, and no further
2472
+ const frozen = line.map(
2473
+ (item, i) =>
2474
+ !Number.isNaN(item.frozen) ||
2475
+ (grow ? item.grow : item.shrink) === 0 ||
2476
+ (grow ? item.base > size[i] + NEAR : item.base < size[i] - NEAR),
2477
+ );
2478
+ const free = (): number => {
2479
+ let left = room;
2480
+ for (let i = 0; i < count; i++) left -= frozen[i] ? size[i] : line[i].base;
2481
+ return left;
2482
+ };
2483
+ // step 3
2484
+ const initial = free();
2485
+ const off = new Array<number>(count).fill(0);
2486
+ for (;;) {
2487
+ // step 4a
2488
+ let factors = 0;
2489
+ let open = false;
2490
+ for (let i = 0; i < count; i++) {
2491
+ if (frozen[i]) continue;
2492
+ open = true;
2493
+ factors += grow ? line[i].grow : line[i].shrink;
1598
2494
  }
1599
- if (!shrinks(box, laid)) least += laid.main;
1600
- else {
1601
- const min = leastOf(box, laid, row);
1602
- shrinking += 1;
1603
- least += min;
1604
- if (laid.main > min + 0.01) over = true;
2495
+ if (!open) break;
2496
+ // 4b: flex factors that come to less than 1 share out that part of the
2497
+ // free space the line started with
2498
+ let left = free();
2499
+ if (factors < 1) {
2500
+ const part = initial * factors;
2501
+ if (Math.abs(part) < Math.abs(left)) left = part;
2502
+ }
2503
+ // 4c: shared by the grow factors, or by the shrink factors each times
2504
+ // the item's inner flex base size
2505
+ let scaled = 0;
2506
+ if (!grow) {
2507
+ for (let i = 0; i < count; i++) {
2508
+ if (!frozen[i])
2509
+ scaled += line[i].shrink * (line[i].base - line[i].extra);
2510
+ }
2511
+ }
2512
+ // 4d: each within its limits, and how far that took it
2513
+ let violation = 0;
2514
+ for (let i = 0; i < count; i++) {
2515
+ if (frozen[i]) continue;
2516
+ const item = line[i];
2517
+ let target = item.base;
2518
+ if (grow) target += (left * item.grow) / factors;
2519
+ else if (scaled > 0) {
2520
+ target -=
2521
+ (Math.abs(left) * item.shrink * (item.base - item.extra)) / scaled;
2522
+ }
2523
+ size[i] = Math.max(item.least, Math.min(item.most, target));
2524
+ off[i] = size[i] - target;
2525
+ violation += off[i];
2526
+ }
2527
+ // 4e: freeze those stopped the way most of them were, or all of them
2528
+ for (let i = 0; i < count; i++) {
2529
+ if (frozen[i]) continue;
2530
+ if (violation === 0 || (violation > 0 ? off[i] > 0 : off[i] < 0)) {
2531
+ frozen[i] = true;
2532
+ }
1605
2533
  }
1606
2534
  }
1607
- // two at the least: what is left of a sum of one, less it, is 0 exactly.
1608
- // A line within a thousandth of a pixel of room is short of it: what
1609
- // Yoga makes of that is as far out, and the minimums are that near
1610
- if (shrinking < 2 || !over || !(between + least > room - SHORT)) {
2535
+ return size;
2536
+ }
2537
+
2538
+ /** The least an item may be along the main axis, its border box: the
2539
+ * minimum of its own, and no less than its padding and borders. */
2540
+ function leastOf(box: Box, laid: Laid, row: boolean): number {
2541
+ const extra = row ? box.horizontalExtra : box.verticalExtra;
2542
+ return Number.isNaN(laid.least) ? extra : Math.max(laid.least, extra);
2543
+ }
2544
+
2545
+ /**
2546
+ * Put right the lines of a flex box that wraps on which every item that
2547
+ * may grow is stopped by its maximum and Yoga made them smaller for it;
2548
+ * whether Yoga was asked again. The one line of a box that does not wrap
2549
+ * is shared out by 9.7 here where Yoga's answer may not be its
2550
+ * (`resolveLine`), which puts right this and more. Each such line is laid out with every item of it that
2551
+ * may grow frozen at its maximum, which is where CSS Flexbox leaves them
2552
+ * (9.7, step 4): the line has room left over with every one of them as
2553
+ * large as it goes. It is also where Yoga leaves them itself, where its
2554
+ * rounding is not below 0.
2555
+ *
2556
+ * It is a float's rounding of nothing. Yoga's first pass takes
2557
+ * each item its maximum stops out of the sum of the grow factors, and the
2558
+ * second divides the room still left by what is left of the sum: with
2559
+ * every item stopped, `(a + b + c) - a - b - c` in float32s. Where that is
2560
+ * 0, or a rounding above it, the quotient is vast and each item is held to
2561
+ * its maximum, which is right; where it is a rounding below, each is held
2562
+ * to its minimum. Three items of `flex-grow: 3.361`, `2.417` and `1.988`,
2563
+ * each `max-width: 10px`, in a row 100 wide came out nothing wide, as a
2564
+ * third of such lines of random factors do. Sums of whole numbers, and of
2565
+ * halves and quarters of them, are exact, so a box whose factors are those
2566
+ * is not looked at (`exactSums`).
2567
+ *
2568
+ * What makes a line one to put right is Yoga's answer. Every item of
2569
+ * the line that may grow has a
2570
+ * maximum, and the line has room to spare with each of them at it. And
2571
+ * one of them came out short of its maximum and no larger than its
2572
+ * hypothetical size (9.7, step 2), which Yoga, sharing out room, makes no
2573
+ * item: Yoga is asked for the line with nothing growing, where each item
2574
+ * is that size. No larger rather than smaller, because an item held to
2575
+ * its minimum is often that size: an empty one is nothing wide either
2576
+ * way. Any other line is put back as it was, and the two layouts that
2577
+ * took are the cost of a box with a factor that is not a whole number and
2578
+ * an item short of its maximum; any other box has cost a sum.
2579
+ *
2580
+ * A few of the lines put right are still not a browser's. Once
2581
+ * some of its items are frozen, 9.7 shares out to the rest only the part
2582
+ * of the room their factors come to, where that is less than 1 (step 4,
2583
+ * b), and Yoga has no such rule: an item of `flex-grow: 0.1` left alone
2584
+ * on a line is at its maximum here, as Yoga would have had it, and a
2585
+ * tenth of the room in Chrome.
2586
+ *
2587
+ * A box that wraps has lines like it: the last line of a grid of cards
2588
+ * with a `max-width` has room for every card it has. Yoga breaks lines by
2589
+ * its items' hypothetical sizes, which the layout with nothing growing
2590
+ * gives, and they are broken here as it breaks them (`linesOf`). Freezing
2591
+ * a line's items at their maximums moves no break: the line has room for
2592
+ * every item on it at that size, so it keeps them all, and the item after
2593
+ * it, which did not fit with them smaller, fits no better with them larger.
2594
+ * But Yoga holds a size to an item's maximum before its minimum, so where
2595
+ * the maximum is the smaller — or smaller than the item's padding and
2596
+ * borders — it breaks a line by one size and lays the item out at another:
2597
+ * `max-width: 2px; min-width: 10px` is 10 wide, on a line it was put on
2598
+ * as 2. A box that wraps with an item like that is left as Yoga had it.
2599
+ */
2600
+ function cappedLines(
2601
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
2602
+ row: boolean,
2603
+ /** The flex box's size along the main axis. */
2604
+ room: number,
2605
+ /** The size Yoga breaks lines at: the flex box's where it wraps and has
2606
+ * one, and else Infinity, for the one line. */
2607
+ breaks: number,
2608
+ /** The gap between two items. */
2609
+ gap: number,
2610
+ hold: Hold,
2611
+ /** Lay the flex box out, as its nodes now are. */
2612
+ ask: () => void,
2613
+ ): boolean {
2614
+ // two at the least: what is left of a sum of one, less it, is 0 exactly
2615
+ const growing = items.filter(({ box, laid }) => grows(box, laid));
2616
+ if (growing.length < 2 || exactSums(growing)) return false;
2617
+ // one short of its maximum, as each on such a line is
2618
+ const short = growing.some(
2619
+ ({ box, laid }) => laid.main < mostOf(box, laid, row) - 0.01,
2620
+ );
2621
+ if (!short) return false;
2622
+ if (breaks === Infinity) {
2623
+ // and on the one line, every one of them with a maximum, and room left
2624
+ // over as Yoga has it
2625
+ let used = gap * (items.length - 1);
2626
+ for (const { box, laid } of items) used += laid.main + marginsOf(box, row);
2627
+ const capped = growing.every(({ box, laid }) => !Number.isNaN(laid.most));
2628
+ if (!capped || !(used < room - SHORT)) return false;
2629
+ } else if (
2630
+ // and on lines broken where the layout says, with no item that Yoga
2631
+ // breaks a line by at another size
2632
+ items.some(({ box, laid }) => laid.most < leastOf(box, laid, row))
2633
+ ) {
1611
2634
  return false;
1612
2635
  }
1613
2636
 
1614
- const stopped = items.filter(({ box, laid }) => shrinks(box, laid));
1615
- for (const { node } of stopped) node.setFlexShrink(0);
2637
+ const flexible = items.filter(({ laid }) => Number.isNaN(laid.held));
2638
+ for (const { node } of flexible) {
2639
+ node.setFlexGrow(0);
2640
+ node.setFlexShrink(0);
2641
+ }
1616
2642
  ask();
1617
- // each item its hypothetical size, which one that does not shrink is on
1618
- // a line short of room — where Yoga may have grown it, on a line it took
1619
- // for having some
1620
- let wider = false;
1621
- least = 0;
1622
- for (const { box, node, laid } of items) {
1623
- const hypothetical = row
1624
- ? node.getComputedWidth()
1625
- : node.getComputedHeight();
1626
- if (!shrinks(box, laid)) least += hypothetical;
1627
- else {
1628
- least += leastOf(box, laid, row);
1629
- if (laid.main > hypothetical && !nearly(laid.main, hypothetical)) {
1630
- wider = true;
2643
+ // each item its hypothetical size, on every line
2644
+ const hypothetical = items.map(({ node, laid }) =>
2645
+ row ? node.getComputedWidth() : heightOf(node, laid),
2646
+ );
2647
+ const frozen = new Set<Box>();
2648
+ for (const line of linesOf(items, hypothetical, row, breaks, gap)) {
2649
+ const growers = line.filter((i) => grows(items[i].box, items[i].laid));
2650
+ if (growers.length < 2) continue;
2651
+ // the line with each item that may grow at its maximum: Infinity where
2652
+ // one has none
2653
+ let size = gap * (line.length - 1);
2654
+ let smaller = false;
2655
+ for (const i of line) {
2656
+ const { box, laid } = items[i];
2657
+ size += marginsOf(box, row);
2658
+ if (!grows(box, laid)) {
2659
+ size += hypothetical[i];
2660
+ continue;
2661
+ }
2662
+ const most = mostOf(box, laid, row);
2663
+ size += most;
2664
+ if (
2665
+ laid.main < most - 0.01 &&
2666
+ (laid.main < hypothetical[i] || nearly(laid.main, hypothetical[i]))
2667
+ ) {
2668
+ smaller = true;
1631
2669
  }
1632
2670
  }
2671
+ if (!smaller || !(size < room - SHORT)) continue;
2672
+ for (const i of growers) frozen.add(items[i].box);
1633
2673
  }
1634
- const freeze = wider && between + least > room - SHORT;
1635
- for (const { box, node, laid } of stopped) {
1636
- if (freeze) freezeAt(node, laid, row, leastOf(box, laid, row), hold);
1637
- else node.setFlexShrink(box.style.flexShrink);
2674
+ for (const { box, node, laid } of flexible) {
2675
+ if (frozen.has(box)) {
2676
+ freezeAt(node, laid, row, mostOf(box, laid, row), hold);
2677
+ } else {
2678
+ node.setFlexGrow(box.style.flexGrow);
2679
+ node.setFlexShrink(box.style.flexShrink);
2680
+ }
1638
2681
  }
1639
2682
  ask();
1640
2683
  return true;
1641
2684
  }
1642
2685
 
1643
- /** Whether Yoga may shrink an item: it has a shrink factor, and is not
1644
- * frozen (`freezeAt`). */
1645
- function shrinks(box: Box, laid: Laid): boolean {
1646
- return box.style.flexShrink > 0 && Number.isNaN(laid.held);
2686
+ /**
2687
+ * The lines Yoga breaks a flex box's items into, as indices of them. A line
2688
+ * takes items while their hypothetical sizes, their margins and the gaps
2689
+ * between them come to no more than the size it breaks at, and always
2690
+ * takes one — added up in float32s, in the order Yoga adds them, so that an
2691
+ * item that fits exactly is on the line it is on in Yoga's.
2692
+ */
2693
+ function linesOf(
2694
+ items: readonly { box: Box }[],
2695
+ /** Each item's hypothetical size along the main axis. */
2696
+ sizes: readonly number[],
2697
+ row: boolean,
2698
+ breaks: number,
2699
+ gap: number,
2700
+ ): number[][] {
2701
+ if (breaks === Infinity) return [items.map((_, i) => i)];
2702
+ const f = Math.fround;
2703
+ const limit = f(breaks);
2704
+ const lines: number[][] = [];
2705
+ let line: number[] = [];
2706
+ let used = 0;
2707
+ items.forEach(({ box }, i) => {
2708
+ const size = f(sizes[i]);
2709
+ const style = box.style;
2710
+ const [start, end] = row
2711
+ ? [style.marginLeft, style.marginRight]
2712
+ : [style.marginTop, style.marginBottom];
2713
+ const margin = f(
2714
+ (start === AUTO ? 0 : f(row ? box.marginLeft : box.marginTop)) +
2715
+ (end === AUTO ? 0 : f(row ? box.marginRight : box.marginBottom)),
2716
+ );
2717
+ let lead = line.length ? f(gap) : 0;
2718
+ if (line.length && f(f(f(used + size) + margin) + lead) > limit) {
2719
+ lines.push(line);
2720
+ line = [];
2721
+ used = 0;
2722
+ lead = 0;
2723
+ }
2724
+ used = f(used + f(f(size + margin) + lead));
2725
+ line.push(i);
2726
+ });
2727
+ lines.push(line);
2728
+ return lines;
1647
2729
  }
1648
2730
 
1649
- /** How near to its room a line may be and be short of it (`stoppedLine`). */
1650
- const SHORT = 0.001;
2731
+ /** Whether Yoga may grow an item: it has a grow factor, and is not frozen
2732
+ * (`freezeAt`). */
2733
+ function grows(box: Box, laid: Laid): boolean {
2734
+ return box.style.flexGrow > 0 && Number.isNaN(laid.held);
2735
+ }
1651
2736
 
1652
- /** The least an item may be along the main axis, its border box: the
1653
- * minimum of its own, and no less than its padding and borders. */
1654
- function leastOf(box: Box, laid: Laid, row: boolean): number {
1655
- const extra = row ? box.horizontalExtra : box.verticalExtra;
1656
- return Number.isNaN(laid.least) ? extra : Math.max(laid.least, extra);
2737
+ /** The most an item may be along the main axis, its border box: the
2738
+ * maximum of its own, where it has one, and no less than its least size;
2739
+ * Infinity where it has none. */
2740
+ function mostOf(box: Box, laid: Laid, row: boolean): number {
2741
+ if (Number.isNaN(laid.most)) return Infinity;
2742
+ return Math.max(laid.most, leastOf(box, laid, row));
1657
2743
  }
1658
2744
 
2745
+ /** An item's margins along the main axis, where they are not `auto`: an
2746
+ * `auto` margin is none to the size of its line (CSS Flexbox 8.1). */
2747
+ function marginsOf(box: Box, row: boolean): number {
2748
+ const style = box.style;
2749
+ if (row) {
2750
+ return (
2751
+ (style.marginLeft === AUTO ? 0 : box.marginLeft) +
2752
+ (style.marginRight === AUTO ? 0 : box.marginRight)
2753
+ );
2754
+ }
2755
+ return (
2756
+ (style.marginTop === AUTO ? 0 : box.marginTop) +
2757
+ (style.marginBottom === AUTO ? 0 : box.marginBottom)
2758
+ );
2759
+ }
2760
+
2761
+ /**
2762
+ * Whether Yoga's sums of these items' grow factors are exact: in float32s,
2763
+ * as it adds them, a sum of whole numbers is, and so is one of halves and
2764
+ * quarters of them, and what is left of it less each of its terms is then
2765
+ * 0 exactly (`cappedLines`).
2766
+ */
2767
+ function exactSums(items: readonly { box: Box }[]): boolean {
2768
+ let sum = 0;
2769
+ for (const { box } of items) {
2770
+ const factor = box.style.flexGrow * EXACT;
2771
+ if (!Number.isInteger(factor)) return false;
2772
+ sum += factor;
2773
+ }
2774
+ // every sum along the way a whole number of the parts under a float32's
2775
+ // 24 bits of significand
2776
+ return sum < 2 ** 24;
2777
+ }
2778
+
2779
+ /** The parts of one a grow factor may be a whole number of and have its
2780
+ * sums exact (`exactSums`). */
2781
+ const EXACT = 1024;
2782
+
1659
2783
  /** An item's min-content width, its border box's, taken the once: its
1660
2784
  * content's, whatever width it has of its own (`keywordWidth`'s too). */
1661
2785
  function minContentOf(box: Box, ctx: LayoutContext, laid: Laid): number {
@@ -1750,6 +2874,113 @@ function baselineLines<T extends { box: Box }>(
1750
2874
  return several ? out : null;
1751
2875
  }
1752
2876
 
2877
+ /**
2878
+ * Whether Yoga made a line of a row that wraps the row's height, where its
2879
+ * items make it another (CSS Flexbox 9.4, steps 7 and 8; `layoutFlex`).
2880
+ * Where `align-content` stretches the lines, that is an item stretched
2881
+ * across one that is shorter than its content at the width it was given:
2882
+ * its line is as tall as that, past the box. Where it centres them or ends
2883
+ * them, it is the one line of the row not as tall as the row, which moves
2884
+ * it from the row's start — and so where it spaces them out around or
2885
+ * evenly, which centres one line (CSS Box Alignment 3, 5.1). Lines it
2886
+ * starts at the row's start are there in Yoga's layout as well, and so is
2887
+ * the one line `space-between` starts there.
2888
+ */
2889
+ function heldLine(
2890
+ container: Box,
2891
+ ctx: LayoutContext,
2892
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
2893
+ /** The row's content height. */
2894
+ height: number,
2895
+ /** The row's content width, the items' containing block's. */
2896
+ width: number,
2897
+ /** The gap between two items along the row. */
2898
+ gap: number,
2899
+ ): boolean {
2900
+ const style = container.style;
2901
+ const lines = crossContent(style);
2902
+ if (
2903
+ lines !== 'stretch' &&
2904
+ lines !== 'center' &&
2905
+ lines !== 'flex-end' &&
2906
+ lines !== 'space-around' &&
2907
+ lines !== 'space-evenly'
2908
+ ) {
2909
+ return false;
2910
+ }
2911
+ const stretched = items.filter(({ box }) => stretches(box, style));
2912
+ if (stretched.length === 0) return false;
2913
+ if (lines === 'stretch') {
2914
+ return stretched.some(
2915
+ ({ box, node, laid }) =>
2916
+ naturalAcross(box, ctx, laid, width) > heightOf(node, laid) + 0.01,
2917
+ );
2918
+ }
2919
+ // one line, which the items come to no more than the row's width along:
2920
+ // Yoga made a line each of them only where they come to more
2921
+ let along = gap * (items.length - 1);
2922
+ for (const { box, laid } of items) along += laid.main + marginsOf(box, true);
2923
+ if (along > width + 0.01) return false;
2924
+ // as tall as its tallest item, as Yoga has them: it lays them out at
2925
+ // their content's height where `align-content` does not stretch lines
2926
+ let line = 0;
2927
+ for (const { box, node, laid } of items) {
2928
+ line = Math.max(line, heightOf(node, laid) + marginsOf(box, false));
2929
+ }
2930
+ return Math.abs(line - height) > 0.01;
2931
+ }
2932
+
2933
+ /**
2934
+ * A row item's border-box height as its content makes it at the width the
2935
+ * flex layout gave it, within its limits: the layout `placeItems` gives it
2936
+ * there, made now where it has none (`layoutItemAt`), and a replaced one's
2937
+ * through its ratio or its natural height.
2938
+ */
2939
+ function naturalAcross(
2940
+ box: Box,
2941
+ ctx: LayoutContext,
2942
+ laid: Laid,
2943
+ /** The flex box's content width, the item's containing block's. */
2944
+ containing: number,
2945
+ ): number {
2946
+ const width = meant(laid.main, laid.set, laid.stretch, laid.width, laid.held);
2947
+ if (box.kind === 'replaced') {
2948
+ const own = box.intrinsic;
2949
+ const natural =
2950
+ heightThroughRatio(box, width) ??
2951
+ (own && !(own.missing & 2) ? own.height + box.verticalExtra : null);
2952
+ return natural === null ? 0 : clampHeight(box, natural);
2953
+ }
2954
+ if (box.kind === 'text' || box.kind === 'break') return 0;
2955
+ const kept = naturalHeight(box, ctx, width, containing, laid);
2956
+ if (!Number.isNaN(kept)) return kept;
2957
+ layoutNaturally(box, ctx, width, containing, laid);
2958
+ return box.height;
2959
+ }
2960
+
2961
+ /**
2962
+ * The alignment an item with an `auto` margin across a row that wraps comes
2963
+ * to in its line (CSS Flexbox 8.1, 9.6 step 13), as Yoga is told it
2964
+ * (`layoutFlex`): at the end where the margin at the line's cross start is
2965
+ * `auto`, at the start where the one at its end is, and centred where both
2966
+ * are; null where neither is. Its line's cross start is the box's bottom
2967
+ * where its lines wrap in reverse.
2968
+ *
2969
+ * The line is as tall as its tallest item, margins and all, so the item
2970
+ * comes to no more than it, and the margins take a share of no less than
2971
+ * nothing: the alignment puts it where they would.
2972
+ */
2973
+ function autoAcross(
2974
+ item: ComputedStyle,
2975
+ reverse: boolean,
2976
+ ): 'flex-start' | 'flex-end' | 'center' | null {
2977
+ const top = item.marginTop === AUTO;
2978
+ const bottom = item.marginBottom === AUTO;
2979
+ if (top && bottom) return 'center';
2980
+ if (!top && !bottom) return null;
2981
+ return top !== reverse ? 'flex-end' : 'flex-start';
2982
+ }
2983
+
1753
2984
  /**
1754
2985
  * How far an item is from where it belongs across its line, where Yoga
1755
2986
  * (3.2.1) aligned the lines in the pass it takes for a box that wraps or
@@ -1758,9 +2989,17 @@ function baselineLines<T extends { box: Box }>(
1758
2989
  * none at either — `items-start` in a wrapping row put every card's top
1759
2990
  * margin under it. By the margin at the start of the line's cross axis, and
1760
2991
  * by half what it exceeds the one at the end by, as Yoga holds them: for an
1761
- * item set at its line's start (`started`), its baseline's. A column's,
1762
- * whose cross axis runs right to left, and a box that wraps in reverse are
1763
- * left as Yoga has them.
2992
+ * item set at its line's start (`started`), its baseline's.
2993
+ *
2994
+ * A row whose lines wrap in reverse Yoga lays out as though they wrapped
2995
+ * forward, and then turns each item over across the box, its margins with
2996
+ * it: each is where the other margin would have put it. One at its line's
2997
+ * start, the bottom, stands on nothing; one at its end, the top, or
2998
+ * stretched, is its bottom margin down from its line's top where it is its
2999
+ * top one; and one centred, half the difference. A column's, whose cross
3000
+ * axis runs right to left, or that wraps in reverse, is left as Yoga has
3001
+ * it, and so is an item an `auto` margin aligns in a row that does not
3002
+ * wrap.
1764
3003
  */
1765
3004
  function lineMarginFix(
1766
3005
  container: ComputedStyle,
@@ -1769,23 +3008,35 @@ function lineMarginFix(
1769
3008
  row: boolean,
1770
3009
  started = false,
1771
3010
  ): number {
1772
- if (container.flexWrap === 'wrap-reverse') return 0;
1773
- if (!row && container.direction === 'rtl') return 0;
3011
+ const reverse = container.flexWrap === 'wrap-reverse';
3012
+ if (!row && (reverse || container.direction === 'rtl')) return 0;
1774
3013
  const style = item.style;
1775
- // a margin that is `auto` takes the item over its alignment
3014
+ // a margin that is `auto` takes the item over its alignment, which Yoga
3015
+ // is told it comes to in a row that wraps (`autoAcross`)
1776
3016
  const lead = row ? style.marginTop : style.marginLeft;
1777
3017
  const trail = row ? style.marginBottom : style.marginRight;
1778
- if (lead === AUTO || trail === AUTO) return 0;
1779
- const align = started
1780
- ? 'flex-start'
1781
- : style.alignSelf === AUTO
1782
- ? container.alignItems
1783
- : style.alignSelf;
1784
- if (align !== 'flex-start' && align !== 'center') return 0;
3018
+ let align: string | null;
3019
+ if (started) align = 'flex-start';
3020
+ else if (lead === AUTO || trail === AUTO) {
3021
+ align =
3022
+ row && container.flexWrap !== 'nowrap'
3023
+ ? autoAcross(style, reverse)
3024
+ : null;
3025
+ } else {
3026
+ align = style.alignSelf === AUTO ? container.alignItems : style.alignSelf;
3027
+ }
1785
3028
  const leading = node.getComputedMargin(row ? Y.EDGE_TOP : Y.EDGE_LEFT);
1786
- if (align === 'flex-start') return leading;
1787
3029
  const trailing = node.getComputedMargin(row ? Y.EDGE_BOTTOM : Y.EDGE_RIGHT);
1788
- return (leading - trailing) / 2;
3030
+ switch (align) {
3031
+ case 'flex-start':
3032
+ return reverse ? -trailing : leading;
3033
+ case 'center':
3034
+ return (leading - trailing) / 2;
3035
+ case 'flex-end':
3036
+ case 'stretch':
3037
+ return reverse ? leading - trailing : 0;
3038
+ }
3039
+ return 0;
1789
3040
  }
1790
3041
 
1791
3042
  /** An item's first baseline, down from its border edge: its content's
@@ -1799,6 +3050,232 @@ function itemBaseline(item: Box): number {
1799
3050
  return item.height;
1800
3051
  }
1801
3052
 
3053
+ /**
3054
+ * The items of a column that wraps that Yoga (3.2.1) lays out at another
3055
+ * height than it sized them at, as it stretches them across their lines:
3056
+ * each it aligns `stretch`, with no width of its own, whose left and right
3057
+ * margins come to another sum than its top and bottom ones.
3058
+ *
3059
+ * Yoga aligns the lines of a box that wraps in a pass of its own, after it
3060
+ * has sized and placed every item down them, and lays each item it
3061
+ * stretches out again there at its line's width — and at its height plus
3062
+ * the margins across the line, where it means the ones down it
3063
+ * (CalculateLayout.cpp's multi-line `Align::Stretch`, still so on Yoga's
3064
+ * main). A leaf laid out at a width and a height is that size, so the item
3065
+ * came out its side margins taller and its top and bottom ones shorter
3066
+ * than the height its line was shared out to, which its place and the
3067
+ * next item's had been worked out from. The pass lays an item out again
3068
+ * wherever its side margins are not nothing, or its line is another width
3069
+ * than the one it was measured at, so `margin: 0 5px` on a line of words
3070
+ * was 28 tall where Chrome's is 18, and `margin: 4px 0 2px` in a column
3071
+ * with two lines 6 short; where the two sums are the same, the pass gives
3072
+ * the height back unchanged.
3073
+ */
3074
+ function stretchedDown(
3075
+ root: YogaNode,
3076
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
3077
+ ): { box: Box; node: YogaNode; laid: Laid }[] {
3078
+ const lines = root.getAlignItems();
3079
+ return items.filter(({ box, node }) => {
3080
+ const own = node.getAlignSelf();
3081
+ if ((own === Y.ALIGN_AUTO ? lines : own) !== Y.ALIGN_STRETCH) return false;
3082
+ const width = node.getWidth().unit;
3083
+ if (width !== Y.UNIT_AUTO && width !== Y.UNIT_UNDEFINED) return false;
3084
+ // as Yoga adds them up, an `auto` margin as nothing
3085
+ const style = box.style;
3086
+ const of = (len: unknown, px: number) => (len === AUTO ? 0 : px);
3087
+ const across =
3088
+ of(style.marginLeft, box.marginLeft) +
3089
+ of(style.marginRight, box.marginRight);
3090
+ const down =
3091
+ of(style.marginTop, box.marginTop) +
3092
+ of(style.marginBottom, box.marginBottom);
3093
+ return Math.abs(across - down) >= 0.0001;
3094
+ });
3095
+ }
3096
+
3097
+ /**
3098
+ * Lay a column that wraps out, keeping for each item Yoga stretches across
3099
+ * its line the height it sized it at (`stretchedDown`, `Laid.unstretched`).
3100
+ *
3101
+ * Yoga is asked again with each such item set at its line's start, which
3102
+ * its multi-line pass does not lay out again: each is as tall as its line
3103
+ * made it. Its flex base size has to come out the same for that, so its
3104
+ * lines break and share out the same — and where Yoga hands the column's
3105
+ * width over as a size, it measures an item it stretches at that width, and
3106
+ * one it does not at its content's, so there the item is handed the width
3107
+ * as its own too. Then Yoga is asked once more as it was, so the lines and
3108
+ * the widths are its own, and only the heights are taken from the second
3109
+ * layout. Where an item's place down its line moved between the two, a
3110
+ * base moved too, and Yoga's height is kept, as it was.
3111
+ */
3112
+ function unstretch(
3113
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
3114
+ stretched: readonly { box: Box; node: YogaNode; laid: Laid }[],
3115
+ /** The column's content width, where Yoga is handed it as a size, and
3116
+ * else null. */
3117
+ width: number | null,
3118
+ layout: () => void,
3119
+ ): void {
3120
+ layout();
3121
+ const tops = items.map(({ node }) => node.getComputedTop());
3122
+ const aligned = stretched.map(({ box, node }) => {
3123
+ const own = node.getAlignSelf();
3124
+ node.setAlignSelf(Y.ALIGN_FLEX_START);
3125
+ if (width !== null) node.setWidth(measuredAcross(box, node, width));
3126
+ return own;
3127
+ });
3128
+ layout();
3129
+ const kept = items.every(
3130
+ ({ node }, i) => Math.abs(node.getComputedTop() - tops[i]) < 0.01,
3131
+ );
3132
+ const heights = stretched.map(({ node }) => node.getComputedHeight());
3133
+ stretched.forEach(({ node }, i) => {
3134
+ node.setAlignSelf(aligned[i]);
3135
+ if (width !== null) node.setWidthAuto();
3136
+ });
3137
+ layout();
3138
+ stretched.forEach(({ laid }, i) => {
3139
+ laid.unstretched = kept ? heights[i] : NaN;
3140
+ });
3141
+ }
3142
+
3143
+ /** The border-box width Yoga measures a column's item at for its flex base
3144
+ * size where it stretches it across a column of a width: the column's,
3145
+ * less its margins across, within its maximum. */
3146
+ function measuredAcross(box: Box, node: YogaNode, width: number): number {
3147
+ const style = box.style;
3148
+ const of = (len: unknown, px: number) => (len === AUTO ? 0 : px);
3149
+ const across =
3150
+ of(style.marginLeft, box.marginLeft) +
3151
+ of(style.marginRight, box.marginRight);
3152
+ const most = node.getMaxWidth();
3153
+ const limit =
3154
+ most.unit === Y.UNIT_POINT
3155
+ ? most.value
3156
+ : most.unit === Y.UNIT_PERCENT
3157
+ ? (most.value / 100) * width
3158
+ : Infinity;
3159
+ return Math.min(width - across, limit);
3160
+ }
3161
+
3162
+ /** An item's border-box height, as Yoga sized it: the one it had before its
3163
+ * line stretched it, where that laid it out at another (`unstretch`,
3164
+ * `unspace`). */
3165
+ function heightOf(node: YogaNode, laid: Laid): number {
3166
+ return Number.isNaN(laid.unstretched)
3167
+ ? node.getComputedHeight()
3168
+ : laid.unstretched;
3169
+ }
3170
+
3171
+ /** Whether `align-content` spaces a flex box's lines out, putting what
3172
+ * room it has past them between them and around them. */
3173
+ function spacesLines(style: ComputedStyle): boolean {
3174
+ const lines = crossContent(style);
3175
+ return (
3176
+ lines === 'space-between' ||
3177
+ lines === 'space-around' ||
3178
+ lines === 'space-evenly'
3179
+ );
3180
+ }
3181
+
3182
+ /**
3183
+ * Lay a row that wraps out, its lines spaced out by `align-content`,
3184
+ * keeping each item Yoga stretches across its line as tall as the line
3185
+ * (`Laid.unstretched`; CSS Flexbox 9.4, step 11).
3186
+ *
3187
+ * Yoga (3.2.1) sets the lines of a box that wraps in a pass of its own,
3188
+ * after it has sized them, and lays each item it stretches out again there
3189
+ * as tall as its line and the room it puts after the line as well
3190
+ * (CalculateLayout.cpp's multi-line `Align::Stretch`, still so on Yoga's
3191
+ * main). Each line was where it belongs, and every item on it that is not
3192
+ * stretched, but a stretched item ran down to the next line: two lines of
3193
+ * 18px items in a row 100 tall came to 82 apiece with `space-between`. So
3194
+ * Yoga is asked with the lines set at the row's start, where it puts no
3195
+ * room between them and each item is as tall as its line. Where the lines
3196
+ * leave the row no room (`linesReach`), that is the layout the spacing
3197
+ * makes too, and is kept. Where they do, Yoga is asked again as the row
3198
+ * is, for where the lines and the items are, and the heights are kept from
3199
+ * the first layout.
3200
+ */
3201
+ function unspace(
3202
+ root: YogaNode,
3203
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
3204
+ /** Whether the lines run bottom to top. */
3205
+ reverse: boolean,
3206
+ layout: () => void,
3207
+ ): void {
3208
+ for (const { laid } of items) laid.unstretched = NaN;
3209
+ // each item Yoga stretches: `stretch`, with no height of its own
3210
+ const align = root.getAlignItems();
3211
+ const stretched = items.filter(({ node }) => {
3212
+ const own = node.getAlignSelf();
3213
+ if ((own === Y.ALIGN_AUTO ? align : own) !== Y.ALIGN_STRETCH) return false;
3214
+ const height = node.getHeight().unit;
3215
+ return height === Y.UNIT_AUTO || height === Y.UNIT_UNDEFINED;
3216
+ });
3217
+ if (stretched.length === 0) {
3218
+ layout();
3219
+ return;
3220
+ }
3221
+ const lines = root.getAlignContent();
3222
+ root.setAlignContent(Y.ALIGN_FLEX_START);
3223
+ layout();
3224
+ root.setAlignContent(lines);
3225
+ if (!(root.getComputedHeight() - linesReach(root, items, reverse) > NEAR)) {
3226
+ return;
3227
+ }
3228
+ const heights = stretched.map(({ node }) => node.getComputedHeight());
3229
+ layout();
3230
+ stretched.forEach(({ node, laid }, i) => {
3231
+ if (Math.abs(node.getComputedHeight() - heights[i]) >= NEAR) {
3232
+ laid.unstretched = heights[i];
3233
+ }
3234
+ });
3235
+ }
3236
+
3237
+ /**
3238
+ * How far down a row that wraps its lines reach, at the least, in the
3239
+ * layout Yoga made of it with the lines at its start (`unspace`): the most,
3240
+ * over its items, of where the item's line ends at the least. An item is as
3241
+ * tall as its line with its margins where it is stretched across it, and
3242
+ * no taller where it is not, and Yoga ends it at its line's end, or with
3243
+ * its margins at the line's start, or centres it with neither
3244
+ * (`lineMarginFix`). Read with the lines turned back the right way up where
3245
+ * they run bottom to top.
3246
+ */
3247
+ function linesReach(
3248
+ root: YogaNode,
3249
+ items: readonly { box: Box; node: YogaNode }[],
3250
+ reverse: boolean,
3251
+ ): number {
3252
+ const height = root.getComputedHeight();
3253
+ const lines = root.getAlignItems();
3254
+ let reach = 0;
3255
+ for (const { box, node } of items) {
3256
+ const tall = node.getComputedHeight();
3257
+ const top = reverse
3258
+ ? height - node.getComputedTop() - tall
3259
+ : node.getComputedTop();
3260
+ const style = box.style;
3261
+ const above = style.marginTop === AUTO ? 0 : box.marginTop;
3262
+ const below = style.marginBottom === AUTO ? 0 : box.marginBottom;
3263
+ const own = node.getAlignSelf();
3264
+ const align = own === Y.ALIGN_AUTO ? lines : own;
3265
+ // where Yoga set it at its line's start with no margin above it, the
3266
+ // line runs on past it by both its margins at the least, and where it
3267
+ // centred it, by half of them
3268
+ const lead =
3269
+ align === Y.ALIGN_FLEX_START
3270
+ ? above
3271
+ : align === Y.ALIGN_CENTER
3272
+ ? (above - below) / 2
3273
+ : 0;
3274
+ reach = Math.max(reach, top + tall + below + lead);
3275
+ }
3276
+ return reach;
3277
+ }
3278
+
1802
3279
  /** Whether an item is stretched across its line: `stretch`, its own or its
1803
3280
  * container's, with no height of its own and no `auto` margin across. */
1804
3281
  function stretches(
@@ -1821,6 +3298,158 @@ function stretches(
1821
3298
  style.marginRight !== AUTO;
1822
3299
  }
1823
3300
 
3301
+ /** Whether a flex box laid out at a width is a column that wraps, whose
3302
+ * lines are as wide as their items and not as the box, and are placed
3303
+ * across it here (`crossLines`). */
3304
+ function wrapsColumn(style: ComputedStyle, width: number): boolean {
3305
+ return (
3306
+ Number.isFinite(width) &&
3307
+ !style.flexDirection.startsWith('row') &&
3308
+ style.flexWrap !== 'nowrap'
3309
+ );
3310
+ }
3311
+
3312
+ /** Where an item is across a column that wraps (`crossLines`): its border
3313
+ * box's left edge from the flex box's content box's, and its width. */
3314
+ interface Across {
3315
+ x: number;
3316
+ width: number;
3317
+ }
3318
+
3319
+ /**
3320
+ * Place a column that wraps across: its lines across the box, and each
3321
+ * item across its line (CSS Flexbox 9.4, steps 8 to 11, and 9.6, steps 13
3322
+ * to 16). From the layout Yoga made of it with every item at its line's
3323
+ * start (`layoutFlex`), in which each item is as wide as it is before it is
3324
+ * stretched, `fit-content` or its own width within its limits, and each
3325
+ * line starts its gap and a pixel past the one before, which is how the
3326
+ * items of one line are told from the next one's.
3327
+ *
3328
+ * A line is as wide as its widest item, margins and all. `align-content`
3329
+ * (its `start` and `end` the box's own sides, `crossContent`) shares out
3330
+ * what the box has past its lines: `stretch` to each line alike,
3331
+ * where they are short of the box; `center` and `flex-end` whether they are
3332
+ * or not, so that lines wider than the box run past both its sides, or its
3333
+ * start; and the spacing values only where there is room, setting the lines
3334
+ * at the box's start where there is none (CSS Box Alignment 3, 4.3:
3335
+ * `space-between` falls back to `flex-start`, `space-around` and
3336
+ * `space-evenly` to `safe center`, the start where the lines overflow). An
3337
+ * item with an `auto` margin across its line takes the room the line has
3338
+ * past it on that side (8.1), and none where there is none; a stretched
3339
+ * item is as wide as its line less its margins, within its limits (9.4,
3340
+ * step 11), and as tall as the column made it; and the rest are where
3341
+ * `align-self` says, a baseline at the line's start, as a column has none
3342
+ * across it to align by.
3343
+ */
3344
+ function crossLines(
3345
+ style: ComputedStyle,
3346
+ items: readonly { box: Box; node: YogaNode; laid: Laid }[],
3347
+ /** The flex box's content width. */
3348
+ width: number,
3349
+ /** The gap between two lines. */
3350
+ gap: number,
3351
+ ): Map<Box, Across> {
3352
+ // the lines start at the right where the box runs right to left, or
3353
+ // where they wrap in reverse, and at the left where both are so
3354
+ const fromRight =
3355
+ (style.direction === 'rtl') !== (style.flexWrap === 'wrap-reverse');
3356
+ const lines: number[][] = [];
3357
+ const sizes: number[] = [];
3358
+ // each item's margins across, from its line's start, an `auto` one none
3359
+ const lead: number[] = [];
3360
+ const trail: number[] = [];
3361
+ let last = NaN;
3362
+ items.forEach(({ box, node, laid }, i) => {
3363
+ const yoga = node.getComputedWidth();
3364
+ const left = node.getComputedLeft();
3365
+ const start = fromRight ? width - left - yoga : left;
3366
+ if (!(Math.abs(start - last) < 0.5)) lines.push([]);
3367
+ last = start;
3368
+ lines[lines.length - 1].push(i);
3369
+ sizes.push(meant(yoga, laid.set, laid.stretch, laid.width));
3370
+ const own = box.style;
3371
+ const l = own.marginLeft === AUTO ? 0 : box.marginLeft;
3372
+ const r = own.marginRight === AUTO ? 0 : box.marginRight;
3373
+ lead.push(fromRight ? r : l);
3374
+ trail.push(fromRight ? l : r);
3375
+ });
3376
+ const across = lines.map((line) =>
3377
+ line.reduce((most, i) => Math.max(most, sizes[i] + lead[i] + trail[i]), 0),
3378
+ );
3379
+ const count = lines.length;
3380
+ let room = width - gap * (count - 1);
3381
+ for (const size of across) room -= size;
3382
+ let at = 0;
3383
+ let between = gap;
3384
+ switch (crossContent(style)) {
3385
+ case 'flex-end':
3386
+ at = room;
3387
+ break;
3388
+ case 'center':
3389
+ at = room / 2;
3390
+ break;
3391
+ case 'stretch':
3392
+ if (room > 0) across.forEach((_, k) => (across[k] += room / count));
3393
+ break;
3394
+ case 'space-between':
3395
+ if (room > 0 && count > 1) between += room / (count - 1);
3396
+ break;
3397
+ case 'space-around':
3398
+ if (room > 0) {
3399
+ at = room / count / 2;
3400
+ between += room / count;
3401
+ }
3402
+ break;
3403
+ case 'space-evenly':
3404
+ if (room > 0) {
3405
+ at = room / (count + 1);
3406
+ between += room / (count + 1);
3407
+ }
3408
+ break;
3409
+ }
3410
+ const placed = new Map<Box, Across>();
3411
+ lines.forEach((line, k) => {
3412
+ const size = across[k];
3413
+ for (const i of line) {
3414
+ const { box, node } = items[i];
3415
+ const own = box.style;
3416
+ let itemWidth = sizes[i];
3417
+ let offset = lead[i];
3418
+ const free = size - itemWidth - lead[i] - trail[i];
3419
+ const before = fromRight ? own.marginRight : own.marginLeft;
3420
+ const after = fromRight ? own.marginLeft : own.marginRight;
3421
+ if (before === AUTO || after === AUTO) {
3422
+ if (free > 0 && before === AUTO) {
3423
+ offset += after === AUTO ? free / 2 : free;
3424
+ }
3425
+ } else if (stretches(box, style, false) && !own.widthKeyword) {
3426
+ const most = pointsOf(node.getMaxWidth());
3427
+ const least = pointsOf(node.getMinWidth());
3428
+ itemWidth = size - lead[i] - trail[i];
3429
+ if (itemWidth > most) itemWidth = most;
3430
+ if (itemWidth < least) itemWidth = least;
3431
+ itemWidth = Math.max(itemWidth, box.horizontalExtra);
3432
+ } else {
3433
+ const align = own.alignSelf === AUTO ? style.alignItems : own.alignSelf;
3434
+ if (align === 'center') offset += free / 2;
3435
+ else if (align === 'flex-end') offset += free;
3436
+ }
3437
+ const start = at + offset;
3438
+ placed.set(box, {
3439
+ x: fromRight ? width - start - itemWidth : start,
3440
+ width: itemWidth,
3441
+ });
3442
+ }
3443
+ at += size + between;
3444
+ });
3445
+ return placed;
3446
+ }
3447
+
3448
+ /** A length Yoga holds in points, or NaN where it holds none. */
3449
+ function pointsOf(value: { unit: number; value: number }): number {
3450
+ return value.unit === Y.UNIT_POINT ? value.value : NaN;
3451
+ }
3452
+
1824
3453
  /** How deep flex boxes are laid out by Yoga, one inside another's measure
1825
3454
  * (`layoutFlex`). */
1826
3455
  const FLEX_DEPTH = 64;
@@ -1883,6 +3512,20 @@ function mainJustify(style: ComputedStyle): string {
1883
3512
  return start !== reverse ? 'flex-start' : 'flex-end';
1884
3513
  }
1885
3514
 
3515
+ /**
3516
+ * An `align-content` as the cross axis takes it (CSS Box Alignment 3,
3517
+ * 5.1): `start` and `end` are the box's own edges, which are its lines'
3518
+ * cross ends turned round where the box wraps in reverse — `end` in a row
3519
+ * `wrap-reverse` is the bottom, where its lines start.
3520
+ */
3521
+ function crossContent(style: ComputedStyle): string {
3522
+ const lines = style.alignContent;
3523
+ if (lines !== 'start' && lines !== 'end') return lines;
3524
+ return (lines === 'start') === (style.flexWrap !== 'wrap-reverse')
3525
+ ? 'flex-start'
3526
+ : 'flex-end';
3527
+ }
3528
+
1886
3529
  const JUSTIFY: Record<string, number> = {
1887
3530
  'flex-start': Y?.JUSTIFY_FLEX_START,
1888
3531
  'flex-end': Y?.JUSTIFY_FLEX_END,
@@ -1900,4 +3543,5 @@ const ALIGN: Record<string, number> = {
1900
3543
  baseline: Y?.ALIGN_BASELINE,
1901
3544
  'space-between': Y?.ALIGN_SPACE_BETWEEN,
1902
3545
  'space-around': Y?.ALIGN_SPACE_AROUND,
3546
+ 'space-evenly': Y?.ALIGN_SPACE_EVENLY,
1903
3547
  };