react-x11 2.11.0 → 2.13.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 (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +12 -4
  3. package/src/Reconciler.js +19 -31
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/appcontext.js +59 -30
  7. package/src/bootstrap.js +14 -0
  8. package/src/clientmessage.js +1 -1
  9. package/src/cocoa/app.js +303 -49
  10. package/src/cocoa/bezels.js +175 -30
  11. package/src/cocoa/dnd.js +27 -13
  12. package/src/cocoa/fonts.js +3 -3
  13. package/src/cocoa/glarea.js +24 -5
  14. package/src/cocoa/main.d.ts +8 -0
  15. package/src/cocoa/main.js +43 -0
  16. package/src/cocoa/overlay.js +159 -0
  17. package/src/cocoa/panehost.js +15 -5
  18. package/src/cocoa/presenter.js +13 -9
  19. package/src/cocoa/promotion.js +17 -7
  20. package/src/cocoa/relaunch.js +207 -0
  21. package/src/cocoa/threaded.js +246 -0
  22. package/src/cocoa/window.js +256 -42
  23. package/src/components/Select.js +2 -2
  24. package/src/components/anchor.js +3 -3
  25. package/src/components/native.js +12 -7
  26. package/src/components/theme.js +2 -2
  27. package/src/debug.js +1 -1
  28. package/src/decorations.js +1 -1
  29. package/src/editmenu.js +2 -2
  30. package/src/embedding.js +31 -0
  31. package/src/errors.js +46 -0
  32. package/src/events.js +78 -18
  33. package/src/foreignnodes.js +59 -5
  34. package/src/frames.js +2 -2
  35. package/src/glnodes.js +172 -41
  36. package/src/gloverlay.js +383 -0
  37. package/src/grid.js +1653 -0
  38. package/src/host.d.ts +230 -1
  39. package/src/host.js +11 -3
  40. package/src/imagesource.js +1 -1
  41. package/src/index.d.ts +34 -4
  42. package/src/index.js +9 -1
  43. package/src/layouts.js +721 -0
  44. package/src/node.d.ts +16 -3
  45. package/src/node.js +19 -21
  46. package/src/nodes/animation.js +644 -0
  47. package/src/nodes/box.js +21 -0
  48. package/src/nodes/boxpaint.js +473 -0
  49. package/src/nodes/canvas.js +269 -0
  50. package/src/nodes/cascade.js +600 -0
  51. package/src/nodes/damage.js +183 -0
  52. package/src/nodes/edithistory.js +124 -0
  53. package/src/nodes/editmenupopup.js +260 -0
  54. package/src/nodes/hittest.js +185 -0
  55. package/src/nodes/image.js +266 -0
  56. package/src/nodes/install.js +75 -0
  57. package/src/nodes/invalidate.js +465 -0
  58. package/src/nodes/kinds.js +31 -0
  59. package/src/nodes/layout.js +439 -0
  60. package/src/nodes/layouthost.js +949 -0
  61. package/src/nodes/node.js +868 -0
  62. package/src/nodes/paint.js +466 -0
  63. package/src/nodes/position.js +366 -0
  64. package/src/nodes/preedit.js +127 -0
  65. package/src/nodes/queries.js +330 -0
  66. package/src/nodes/rects.js +102 -0
  67. package/src/nodes/scrollable.js +891 -0
  68. package/src/nodes/scrollbars.js +138 -0
  69. package/src/nodes/scrollblit.js +1034 -0
  70. package/src/nodes/selectable.js +142 -0
  71. package/src/nodes/styling.js +225 -0
  72. package/src/nodes/text.js +649 -0
  73. package/src/nodes/textarea.js +391 -0
  74. package/src/nodes/textinput.js +1146 -0
  75. package/src/nodes/util.js +17 -0
  76. package/src/nodes/window/anchoring.js +161 -0
  77. package/src/nodes/window/capabilities.js +190 -0
  78. package/src/nodes/window/debugpaint.js +83 -0
  79. package/src/nodes/window/droptarget.js +145 -0
  80. package/src/nodes/window/floors.js +577 -0
  81. package/src/nodes/window/flush.js +369 -0
  82. package/src/nodes/window/hints.js +482 -0
  83. package/src/nodes/window/listeners.js +222 -0
  84. package/src/nodes/window/popup.js +71 -0
  85. package/src/nodes/window/size.js +591 -0
  86. package/src/nodes/window/window.js +954 -0
  87. package/src/palette.js +1 -1
  88. package/src/registry.js +7 -3
  89. package/src/styles.js +137 -15
  90. package/src/svgnodes.js +2 -1
  91. package/src/testing/harness.js +2 -2
  92. package/src/textselection.js +5 -3
  93. package/src/trace-registry.js +1 -1
  94. package/src/types/components.d.ts +38 -6
  95. package/src/types/elements.d.ts +26 -14
  96. package/src/types/nodes.d.ts +17 -2
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
@@ -0,0 +1,949 @@
1
+ // Layout hosts (#534): a box whose `layout` names an algorithm registered
2
+ // in layouts.js arranges its children itself. Each child is a yoga root of
3
+ // its own, and the host is a measured leaf in its parent — so what the
4
+ // algorithm decides is on screen in the frame that asked.
5
+ // docs/architecture/custom-layout.md is the design record.
6
+
7
+ import {
8
+ createLayoutNode,
9
+ isMeasuringExactly,
10
+ measuringExactly,
11
+ } from '../styles.js';
12
+ import { Yoga } from '../yoga.js';
13
+ import {
14
+ checkLayoutResult,
15
+ layoutOf,
16
+ resolveOptions,
17
+ unknownLayoutMessage,
18
+ } from '../layouts.js';
19
+ import { reportLayoutError, reportStyleProblem } from '../errors.js';
20
+ import { MEASURE_MODES, measureOffer } from './layout.js';
21
+ import { shallowEqual, NO_CHILDREN } from './util.js';
22
+ import {
23
+ captureLeafHeights,
24
+ declaresOwnMinimum,
25
+ inFlow,
26
+ setMeasuringShrink,
27
+ restoreShrink,
28
+ freezeWidths,
29
+ restoreWidths,
30
+ contentSpan,
31
+ writeFloorsWithin,
32
+ forgetHeightFloors,
33
+ } from './window/floors.js';
34
+
35
+ /**
36
+ * A child, as a layout algorithm sees it (docs/extending.md, "A layout
37
+ * algorithm of your own"): something to measure at a size and read options
38
+ * off, never a node to reach into. Everything it answers is in device pixels
39
+ * and in the child's **margin box** — a margin is part of the room a child
40
+ * takes — so an algorithm that stacks children is right about margins
41
+ * without knowing they exist.
42
+ *
43
+ * One per child, kept on the node: an algorithm is asked several times per
44
+ * pass, and a fresh object per question per child is garbage nobody needs.
45
+ */
46
+ class LayoutChild {
47
+ #node;
48
+
49
+ constructor(node) {
50
+ this.#node = node;
51
+ /** Where this child is in the list the algorithm was handed. */
52
+ this.index = 0;
53
+ }
54
+
55
+ /** The child's `layoutItem`, against the options the layout declared —
56
+ * defaults filled in, lengths in device pixels. */
57
+ get options() {
58
+ return this.#node._layoutItemOptions();
59
+ }
60
+
61
+ /** The child's own resolved style, read-only and in device pixels — what
62
+ * a grid places it by (`gridColumn`, `gridArea`) and aligns it with
63
+ * (`alignSelf`, `justifySelf`). */
64
+ get style() {
65
+ return this.#node.style;
66
+ }
67
+
68
+ /**
69
+ * The margin-box size this child takes under `constraints` — `{ width,
70
+ * height, widthMode, heightMode }`, the vocabulary `measureContent`
71
+ * speaks. An axis given a number and no mode is `'exactly'` that; an axis
72
+ * left out is `'unconstrained'`; `'at-most'` is CSS's fit-content — what
73
+ * the child would like, clamped into the offer, and never below what it
74
+ * cannot be narrower than.
75
+ */
76
+ measure(constraints) {
77
+ return this.#node._measureInHost(constraints ?? NO_CONSTRAINTS);
78
+ }
79
+
80
+ /** `{ minContentWidth, maxContentWidth }`: the narrowest this child can
81
+ * be drawn at, and the width it would take with no bound at all — both
82
+ * margin boxes. The minimum is the content floor the renderer measures
83
+ * for every box (docs/elements.md), so a paragraph's is its longest
84
+ * word. */
85
+ intrinsicSizes() {
86
+ return this.#node._intrinsicInHost();
87
+ }
88
+ }
89
+
90
+ const NO_CONSTRAINTS = Object.freeze({});
91
+
92
+ const NO_OPTIONS = Object.freeze({});
93
+
94
+ const MEASURE_MODE_NAMES = new Set(['exactly', 'at-most', 'unconstrained']);
95
+
96
+ /** Yoga's spelling of a resolved direction, for a tree laid out as a root —
97
+ * a layout host's child inherits its direction through this argument, the
98
+ * way a window's tree inherits the window's. */
99
+ const yogaDirection = (node) =>
100
+ node.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
101
+
102
+ /**
103
+ * Lay out one of a layout host's children as the root it is. Dirt on the
104
+ * root means something inside it changed — content, a style, a floor
105
+ * written — since the sizes remembered for it were taken, and they go
106
+ * (`Node._hostSizesNow`). Every layout of such a root comes through here or
107
+ * through that, which is what makes the dirt a reliable sign: nothing else
108
+ * clears it.
109
+ */
110
+ function layoutHostChild(child, width, height, dir) {
111
+ const yoga = child.yoga;
112
+ if (yoga.isDirty()) child._hostSizes = null;
113
+ child._hostLaidAt = null;
114
+ yoga.calculateLayout(width, height, dir);
115
+ }
116
+
117
+ /** A remembered size, with a bound on how many: an algorithm asks a child a
118
+ * handful of questions a pass, not a new one each run. */
119
+ function remember(sizes, key, size) {
120
+ if (sizes.size >= 32) sizes.clear();
121
+ sizes.set(key, size);
122
+ }
123
+
124
+ const marginBoxWidth = (yoga) =>
125
+ yoga.getComputedWidth() +
126
+ yoga.getComputedMargin(Yoga.EDGE_LEFT) +
127
+ yoga.getComputedMargin(Yoga.EDGE_RIGHT);
128
+
129
+ const marginBoxHeight = (yoga) =>
130
+ yoga.getComputedHeight() +
131
+ yoga.getComputedMargin(Yoga.EDGE_TOP) +
132
+ yoga.getComputedMargin(Yoga.EDGE_BOTTOM);
133
+
134
+ /** Up to a whole pixel, with a thousandth of slack for a sum that should
135
+ * have been whole: a measure that answers a fraction is one yoga divides
136
+ * a rounding residue by (issue #411). */
137
+ const wholePixels = (v) => Math.ceil(v - 1e-3);
138
+
139
+ /**
140
+ * The elements that cannot arrange children with a layout, because they
141
+ * have none to arrange: they measure their own content, or are content.
142
+ * A registered element that implements `measureContent` is the same case,
143
+ * asked of the instance.
144
+ */
145
+ const NO_LAYOUT_KINDS = new Set([
146
+ 'text',
147
+ 'textchunk',
148
+ 'image',
149
+ 'svg',
150
+ 'canvas',
151
+ 'textinput',
152
+ 'textarea',
153
+ 'glarea',
154
+ 'foreign',
155
+ ]);
156
+
157
+ /** Node's half of layout hosts, installed onto `Node.prototype` by node.js. */
158
+ export class NodeLayoutHost {
159
+ // --- a layout host (docs/styling.md, "Custom layouts") -----------------
160
+ //
161
+ // A node whose style names a `layout` hands its children to that
162
+ // algorithm. In yoga's terms it becomes a measured leaf — its size is
163
+ // what the algorithm answers for the room on offer — and each child
164
+ // becomes a yoga tree of its own, laid out where and how big the
165
+ // algorithm says. Both happen inside the pass that lays the window out,
166
+ // so the arrangement is on screen in the frame that asked for it.
167
+
168
+ /**
169
+ * Hand this node's children to the layout its style names, or take them
170
+ * back. Run from the style funnel whenever `layout` could have moved; a
171
+ * value that is the same layout with the same options costs a compare.
172
+ */
173
+ _syncLayoutHost() {
174
+ const found = layoutOf(this.style);
175
+ let def = null;
176
+ let options = null;
177
+ if (found !== null) {
178
+ const refused = found.conflict ? null : this._layoutRefusal();
179
+ if (found.conflict) {
180
+ reportStyleProblem(
181
+ this,
182
+ found.conflict,
183
+ 'It is laid out as flexbox until the two agree',
184
+ );
185
+ } else if (refused !== null) {
186
+ reportStyleProblem(
187
+ this,
188
+ `react-x11: <${this.kind}> cannot ${
189
+ found.name === 'grid'
190
+ ? 'lay its children out as a grid'
191
+ : 'take a layout'
192
+ } — ${refused}`,
193
+ 'It lays out as it would without one',
194
+ );
195
+ } else if (!found.def) {
196
+ reportStyleProblem(
197
+ this,
198
+ unknownLayoutMessage(found.name),
199
+ 'It is laid out as flexbox instead',
200
+ );
201
+ } else {
202
+ const abandoned = this._layoutAbandoned;
203
+ if (
204
+ abandoned === null ||
205
+ abandoned.def !== found.def ||
206
+ !shallowEqual(abandoned.raw, found.raw)
207
+ ) {
208
+ this._layoutAbandoned = null;
209
+ const resolved = resolveOptions(
210
+ found.def.options,
211
+ found.raw,
212
+ this.scale,
213
+ `<${this.kind} style={{ layout: "${found.name}" }}>`,
214
+ );
215
+ if (resolved.problem) {
216
+ // the one layout whose options live in the style instead
217
+ const grid = found.def.builtin && found.name === 'grid';
218
+ reportStyleProblem(
219
+ this,
220
+ grid
221
+ ? "react-x11: layout: { name: 'grid' } takes no options — a " +
222
+ "grid's tracks are the box's own style: gridTemplateColumns, " +
223
+ 'gridTemplateRows, gridTemplateAreas, gridAutoFlow'
224
+ : resolved.problem,
225
+ grid ? 'They are ignored' : 'It takes its default',
226
+ );
227
+ }
228
+ def = found.def;
229
+ options = resolved.options;
230
+ }
231
+ // …else it is the layout that threw, and stays flexbox until the
232
+ // style names another (`WindowNode._abandonFailedHosts`)
233
+ }
234
+ }
235
+ const host = this._host;
236
+ if (def === null) {
237
+ if (host !== null) this._leaveHost();
238
+ return;
239
+ }
240
+ if (host === null) {
241
+ this._enterHost(found.name, def, options);
242
+ return;
243
+ }
244
+ if (
245
+ host.def !== def ||
246
+ host.scale !== this.scale ||
247
+ !shallowEqual(host.options, options)
248
+ ) {
249
+ host.def = def;
250
+ host.name = found.name;
251
+ host.options = options;
252
+ host.scale = this.scale;
253
+ host.failed = null;
254
+ this._hostChanged();
255
+ }
256
+ }
257
+
258
+ /** Why this node cannot arrange children with a layout, or null when it
259
+ * can — the sentence the report finishes with. */
260
+ _layoutRefusal() {
261
+ if (!this.yoga) return 'it takes no part in layout';
262
+ if (this.isWindow) {
263
+ return `put the layout on a <box> inside the <${this.kind}>`;
264
+ }
265
+ if (
266
+ NO_LAYOUT_KINDS.has(this.kind) ||
267
+ typeof this.measureContent === 'function' ||
268
+ (this._measureFn && this._host === null)
269
+ ) {
270
+ return 'it measures its own content, so it has no children to arrange';
271
+ }
272
+ if (this.isScroller?.()) {
273
+ return (
274
+ 'a scroll pane lays out its viewport; put the layout on a <box> ' +
275
+ 'inside the pane, which is what it scrolls'
276
+ );
277
+ }
278
+ return null;
279
+ }
280
+
281
+ _enterHost(name, def, options) {
282
+ const yoga = this.yoga;
283
+ this._host = {
284
+ name,
285
+ def,
286
+ options,
287
+ scale: this.scale,
288
+ // Set whenever the algorithm was asked a hypothetical — which lays the
289
+ // children out at sizes nothing is drawn at — so the placement after
290
+ // the pass knows it has to put them back.
291
+ measured: true,
292
+ // the content box, and direction, the last placement was made for
293
+ size: null,
294
+ // the children the last call was handed, in order, beside the
295
+ // handles it was handed them as
296
+ flow: NO_CHILDREN,
297
+ handles: NO_CHILDREN,
298
+ // a yoga node standing in for the padding box, holding the absolutely
299
+ // positioned children — which yoga then places by its own rules
300
+ absolute: null,
301
+ // the error the algorithm threw, until the pass that saw it is over
302
+ failed: null,
303
+ };
304
+ // The children leave the flex tree first: a node that measures may have
305
+ // no yoga children, and yoga aborts rather than refuse.
306
+ for (const child of this.children) {
307
+ if (!child.yoga || child.isWindow) continue;
308
+ yoga.removeChild(child.yoga);
309
+ this._adoptHostChild(child);
310
+ }
311
+ this._setMeasureFunc((w, wm, h, hm) => this._measureHost(w, wm, h, hm));
312
+ this.root?._layoutHosts.add(this);
313
+ this._hostChanged();
314
+ }
315
+
316
+ _leaveHost() {
317
+ const host = this._host;
318
+ this._host = null;
319
+ this.yoga.unsetMeasureFunc();
320
+ this._measureFn = null;
321
+ let index = 0;
322
+ for (const child of this.children) {
323
+ if (!child.yoga || child.isWindow) continue;
324
+ if (child._hostAbsolute) {
325
+ host.absolute.removeChild(child.yoga);
326
+ child._hostAbsolute = false;
327
+ }
328
+ child._hostSlot = null;
329
+ this.yoga.insertChild(child.yoga, index++);
330
+ }
331
+ host.absolute?.free();
332
+ this.root?._layoutHosts.delete(this);
333
+ this.root?._failedHosts.delete(this);
334
+ this._hostChanged();
335
+ }
336
+
337
+ /** A child joins the layout: its yoga tree is a root of its own now. Its
338
+ * dirt stops there, so the window looks for it before every pass
339
+ * (`WindowNode._sweepLayoutHosts`) and makes it this node's. */
340
+ _adoptHostChild(child) {
341
+ const cy = child.yoga;
342
+ // A floor written while it was a flex item means nothing to a layout,
343
+ // which sizes the child itself — and would outlast it as a minimum.
344
+ if (child._floorMinW != null) {
345
+ cy.setMinWidth(child.style.minWidth);
346
+ child._floorMinW = undefined;
347
+ }
348
+ if (child._floorMinH != null) {
349
+ cy.setMinHeight(child.style.minHeight);
350
+ child._floorMinH = undefined;
351
+ }
352
+ child._hostSlot = null;
353
+ child._hostSizes = null;
354
+ child._hostSqueezed = false;
355
+ child._hostLaidAt = null;
356
+ if (child.style.position === 'absolute') {
357
+ const holder = this._hostHolder();
358
+ holder.insertChild(cy, holder.getChildCount());
359
+ child._hostAbsolute = true;
360
+ }
361
+ }
362
+
363
+ /** The yoga node the absolutely positioned children are laid out in. */
364
+ _hostHolder() {
365
+ return (this._host.absolute ??= createLayoutNode());
366
+ }
367
+
368
+ /** A layout host is going: its children's trees are roots of their own,
369
+ * which the `freeRecursive` that takes this node's box does not reach.
370
+ * Run after the children's own `destroySubtree`, so a child that was a
371
+ * host itself has let go of its children already. */
372
+ _freeHostTrees() {
373
+ const host = this._host;
374
+ for (const child of this.children) {
375
+ const cy = child.yoga;
376
+ if (!cy || child.isWindow) continue;
377
+ if (child._hostAbsolute) {
378
+ host.absolute.removeChild(cy);
379
+ child._hostAbsolute = false;
380
+ }
381
+ cy.freeRecursive();
382
+ child.yoga = null;
383
+ }
384
+ host.absolute?.free();
385
+ host.absolute = null;
386
+ this.root?._layoutHosts?.delete(this);
387
+ this.root?._failedHosts?.delete(this);
388
+ }
389
+
390
+ /** A child moved in or out of `position: 'absolute'`: in flow the
391
+ * algorithm places it, out of it yoga does, against the padding box. */
392
+ _rehomeHostChild(child) {
393
+ const cy = child.yoga;
394
+ if (!cy || this._host === null) return;
395
+ const absolute = child.style.position === 'absolute';
396
+ if (absolute && !child._hostAbsolute) {
397
+ const holder = this._hostHolder();
398
+ holder.insertChild(cy, holder.getChildCount());
399
+ child._hostAbsolute = true;
400
+ } else if (!absolute && child._hostAbsolute) {
401
+ this._host.absolute.removeChild(cy);
402
+ child._hostAbsolute = false;
403
+ }
404
+ child._hostSlot = null;
405
+ this._hostChanged();
406
+ }
407
+
408
+ /** Something the algorithm reads moved: ask it again next pass. */
409
+ _hostChanged() {
410
+ this._markHostDirty();
411
+ this._invalidateLayout('layout');
412
+ const root = this.root;
413
+ if (root) {
414
+ // a layout arriving, leaving or re-asked is a change to the tree the
415
+ // content floors were measured from, as any style change is
416
+ root._floorsDirty = true;
417
+ root._floorsContentDirty = true;
418
+ }
419
+ }
420
+
421
+ _markHostDirty() {
422
+ if (this._host !== null && this.yoga && !this.destroyed) {
423
+ this.yoga.markDirty();
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Yoga's measure function for a layout host: the algorithm's answer for
429
+ * the room on offer, in the content box. Asked several times per pass —
430
+ * the content floors ask for the smallest the box can be, a flex line for
431
+ * its basis — and each asking lays the children out at that size, so
432
+ * `measured` records that the placement after the pass owes them their
433
+ * real one.
434
+ */
435
+ _measureHost(width, widthMode, height, heightMode) {
436
+ const host = this._host;
437
+ if (host === null) return { width: 0, height: 0 };
438
+ if (heightMode === Yoga.MEASURE_MODE_UNDEFINED) {
439
+ this._floorMeasureMode = MEASURE_MODES[widthMode];
440
+ }
441
+ host.measured = true;
442
+ const result = this._runLayout(
443
+ {
444
+ width: measureOffer(width, widthMode),
445
+ height: measureOffer(height, heightMode),
446
+ widthMode: MEASURE_MODES[widthMode],
447
+ heightMode: MEASURE_MODES[heightMode],
448
+ },
449
+ false,
450
+ );
451
+ return result === null
452
+ ? { width: 0, height: 0 }
453
+ : {
454
+ width: wholePixels(result.width),
455
+ height: wholePixels(result.height),
456
+ };
457
+ }
458
+
459
+ /**
460
+ * One call to the algorithm, over the children in flow. A throw, or an
461
+ * answer that is not a size, is reported and turns the layout off for
462
+ * this node: this pass gets an empty box, and the window lays it out as
463
+ * flexbox before the frame is done (`WindowNode._abandonFailedHosts`).
464
+ */
465
+ _runLayout(constraints, final) {
466
+ const host = this._host;
467
+ if (host.failed !== null) return null;
468
+ const flow = [];
469
+ const handles = [];
470
+ for (const child of this.children) {
471
+ if (!child.yoga || child.isWindow || child.hidden) continue;
472
+ const style = child.style;
473
+ if (style.display === 'none' || style.position === 'absolute') continue;
474
+ const handle = (child._handle ??= new LayoutChild(child));
475
+ handle.index = flow.length;
476
+ flow.push(child);
477
+ handles.push(handle);
478
+ }
479
+ host.flow = flow;
480
+ host.handles = handles;
481
+ try {
482
+ return checkLayoutResult(
483
+ host.def.layout(handles, constraints, host.options, {
484
+ style: this.style,
485
+ scale: this.scale,
486
+ // a mistake the algorithm can lay out around — a grid area nobody
487
+ // named — said once, the way a bad style value is
488
+ report: (message, consequence) =>
489
+ reportStyleProblem(this, message, consequence),
490
+ }),
491
+ handles.length,
492
+ final,
493
+ host.name,
494
+ );
495
+ } catch (error) {
496
+ host.failed = error;
497
+ this.root?._failedHosts.add(this);
498
+ reportLayoutError(
499
+ this,
500
+ `layout "${host.name}"`,
501
+ error,
502
+ 'It is laid out as flexbox until its style names another layout',
503
+ );
504
+ return null;
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Place the children where the algorithm says, in the box the pass gave
510
+ * this node — the final call, with both modes `'exactly'`. Each child's
511
+ * yoga tree is laid out at the size its rect names (an axis the rect
512
+ * leaves out is the child's own), and what `absolutize` reads is the
513
+ * slot: the margin box's corner, from this node's border box, mirrored
514
+ * for a right-to-left box so that an algorithm is written once, from the
515
+ * left, and reads correctly both ways.
516
+ *
517
+ * Skipped when nothing has asked the algorithm anything since the last
518
+ * placement and the box is the size it was: the children are still laid
519
+ * out exactly as that placement left them.
520
+ */
521
+ _placeHostChildren(floors) {
522
+ const host = this._host;
523
+ if (host === null || this.destroyed || this.hidden) return false;
524
+ if (this.style.display === 'none') return false;
525
+ const yoga = this.yoga;
526
+ const width = yoga.getComputedWidth();
527
+ const height = yoga.getComputedHeight();
528
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return false;
529
+ const bl = yoga.getComputedBorder(Yoga.EDGE_LEFT);
530
+ const bt = yoga.getComputedBorder(Yoga.EDGE_TOP);
531
+ const br = yoga.getComputedBorder(Yoga.EDGE_RIGHT);
532
+ const bb = yoga.getComputedBorder(Yoga.EDGE_BOTTOM);
533
+ const left = bl + yoga.getComputedPadding(Yoga.EDGE_LEFT);
534
+ const top = bt + yoga.getComputedPadding(Yoga.EDGE_TOP);
535
+ const cw = Math.max(
536
+ 0,
537
+ width - left - br - yoga.getComputedPadding(Yoga.EDGE_RIGHT),
538
+ );
539
+ const ch = Math.max(
540
+ 0,
541
+ height - top - bb - yoga.getComputedPadding(Yoga.EDGE_BOTTOM),
542
+ );
543
+ const rtl = this.direction === 'rtl';
544
+ const last = host.size;
545
+ if (
546
+ !host.measured &&
547
+ last !== null &&
548
+ last.width === cw &&
549
+ last.height === ch &&
550
+ last.rtl === rtl
551
+ ) {
552
+ return false;
553
+ }
554
+ const result = this._runLayout(
555
+ { width: cw, height: ch, widthMode: 'exactly', heightMode: 'exactly' },
556
+ true,
557
+ );
558
+ host.measured = false;
559
+ host.size = { width: cw, height: ch, rtl };
560
+ if (result === null) return false;
561
+ const dir = rtl ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
562
+ const flow = host.flow;
563
+ const rects = result.children;
564
+ let measuredHeights = false;
565
+ for (let i = 0; i < flow.length; i++) {
566
+ const child = flow[i];
567
+ const r = rects[i];
568
+ // A child that can come out shorter than its content has rows that can
569
+ // be squeezed, which need their floors at the width it gets: one that
570
+ // names a height of its own, or one its rect gives less height than
571
+ // its content takes at that width — asked before it is laid out at
572
+ // the rect, since asking lays it out at its natural height. A
573
+ // stretched row of items sized to fit it, and a masonry's cards,
574
+ // never are.
575
+ const squeezed =
576
+ floors &&
577
+ (typeof child.style.height === 'number' ||
578
+ typeof child.style.maxHeight === 'number' ||
579
+ (r.height != null &&
580
+ (child._hostSqueezed ||
581
+ r.height <
582
+ child._measureInHost(
583
+ r.width == null ? NO_CONSTRAINTS : { width: r.width },
584
+ ).height -
585
+ 0.5)));
586
+ // A tree already laid out at this rect, with nothing inside it changed,
587
+ // is left as it is. Yoga would do the same from its own cache but for
588
+ // the floors pass, which measures off the pixel grid and so voids every
589
+ // cached layout in the window — and each child here is a window-sized
590
+ // layout's worth of calls to find out nothing moved.
591
+ const at = child._hostLaidAt;
592
+ if (
593
+ at === null ||
594
+ at.width !== r.width ||
595
+ at.height !== r.height ||
596
+ at.dir !== dir ||
597
+ child.yoga.isDirty()
598
+ ) {
599
+ layoutHostChild(
600
+ child,
601
+ r.width ?? undefined,
602
+ r.height ?? undefined,
603
+ dir,
604
+ );
605
+ }
606
+ if (
607
+ squeezed &&
608
+ (child._floorH === undefined ||
609
+ child._floorAtW === undefined ||
610
+ Math.abs(child._floorAtW - child.yoga.getComputedWidth()) >= 1)
611
+ ) {
612
+ this._measureHostChildHeights(child, r, dir);
613
+ child._hostSqueezed = true;
614
+ measuredHeights = true;
615
+ }
616
+ child._hostLaidAt = { width: r.width, height: r.height, dir };
617
+ const x = rtl ? cw - r.x - (r.width ?? marginBoxWidth(child.yoga)) : r.x;
618
+ // whole pixels: the child's own tree was rounded from its corner, so
619
+ // a fractional corner would put every edge in it between two pixels
620
+ const slot = { x: left + Math.round(x), y: top + Math.round(r.y) };
621
+ const was = child._hostSlot;
622
+ if (was === null || was.x !== slot.x || was.y !== slot.y) {
623
+ child._hostSlot = slot;
624
+ }
625
+ }
626
+ // The absolutely positioned children, by yoga's own rules against the
627
+ // padding box — which is what the holder stands in for.
628
+ const holder = host.absolute;
629
+ if (holder !== null && holder.getChildCount() > 0) {
630
+ const pw = Math.max(0, width - bl - br);
631
+ const ph = Math.max(0, height - bt - bb);
632
+ holder.setWidth(pw);
633
+ holder.setHeight(ph);
634
+ holder.calculateLayout(pw, ph, dir);
635
+ for (const child of this.children) {
636
+ if (child._hostAbsolute) child._hostSlot = { x: bl, y: bt };
637
+ }
638
+ }
639
+ return measuredHeights;
640
+ }
641
+
642
+ /**
643
+ * The height floors inside one of this host's children, at the width the
644
+ * placement just gave it — a minimum height is always a height for a
645
+ * width (`WindowNode._applyContentFloors`), and a child's width is the
646
+ * algorithm's. Measured the way the window measures its own, off the
647
+ * pixel grid: the tree at its width with no bound on its height, the
648
+ * leaves' heights taken, the widths held and the collapse read back
649
+ * (`contentSpan`). Then the floors inside it are written, and it is laid
650
+ * out again at its rect.
651
+ */
652
+ _measureHostChildHeights(child, r, dir) {
653
+ const root = this.root;
654
+ if (!root) return;
655
+ const cy = child.yoga;
656
+ const stale = root._floorsStale;
657
+ const width = r.width ?? marginBoxWidth(cy);
658
+ // at a new width every height inside it is a question again
659
+ if (child._floorH !== undefined) forgetHeightFloors(child, stale);
660
+ measuringExactly(() => {
661
+ // what it holds now comes off first: a floor read back as content
662
+ // could only ever grow
663
+ writeFloorsWithin(child, 'height', stale);
664
+ layoutHostChild(child, width, undefined, dir);
665
+ const intrinsic = new Map();
666
+ captureLeafHeights(child, intrinsic);
667
+ const frozen = [];
668
+ freezeWidths(child, frozen);
669
+ const shrunk = [];
670
+ setMeasuringShrink(child, 'height', shrunk);
671
+ layoutHostChild(child, width, 0, dir);
672
+ const span = contentSpan(child, 'height', intrinsic, root);
673
+ restoreWidths(frozen);
674
+ restoreShrink(shrunk);
675
+ child._floorH = declaresOwnMinimum(child, 'height')
676
+ ? cy.getComputedHeight()
677
+ : span;
678
+ root._floorsMeasured += 1;
679
+ stale.add(child);
680
+ writeFloorsWithin(child, 'height', stale);
681
+ });
682
+ layoutHostChild(child, r.width ?? undefined, r.height ?? undefined, dir);
683
+ child._floorAtW = cy.getComputedWidth();
684
+ }
685
+
686
+ /** `absolutize`'s walk, for a layout host: each child from the slot its
687
+ * placement gave it. One the algorithm was not handed — hidden — sits at
688
+ * the content box's corner, where nothing is drawn of it. */
689
+ _absolutizeHostChildren() {
690
+ const { x, y } = this.abs;
691
+ const yoga = this.yoga;
692
+ const cx =
693
+ yoga.getComputedBorder(Yoga.EDGE_LEFT) +
694
+ yoga.getComputedPadding(Yoga.EDGE_LEFT);
695
+ const cy =
696
+ yoga.getComputedBorder(Yoga.EDGE_TOP) +
697
+ yoga.getComputedPadding(Yoga.EDGE_TOP);
698
+ for (const child of this.children) {
699
+ if (child.isWindow) continue;
700
+ const slot = child._hostSlot;
701
+ child.absolutize(
702
+ x + (slot === null ? cx : slot.x),
703
+ y + (slot === null ? cy : slot.y),
704
+ );
705
+ }
706
+ }
707
+
708
+ // --- …and a child of one, as its algorithm measures it ---------------
709
+
710
+ /** `LayoutChild.measure`, on the child's own yoga tree. */
711
+ _measureInHost(c) {
712
+ const host = this.parent;
713
+ const yoga = this.yoga;
714
+ const dir = yogaDirection(host);
715
+ const widthMode =
716
+ c.widthMode ??
717
+ (c.width == null || c.width === Infinity ? 'unconstrained' : 'exactly');
718
+ const heightMode =
719
+ c.heightMode ??
720
+ (c.height == null || c.height === Infinity ? 'unconstrained' : 'exactly');
721
+ if (
722
+ !MEASURE_MODE_NAMES.has(widthMode) ||
723
+ !MEASURE_MODE_NAMES.has(heightMode)
724
+ ) {
725
+ throw new TypeError(
726
+ `measure() was asked for modes ${JSON.stringify(widthMode)} / ` +
727
+ `${JSON.stringify(heightMode)} — a mode is 'exactly', 'at-most' ` +
728
+ "or 'unconstrained'",
729
+ );
730
+ }
731
+ const sizes = this._hostSizesNow();
732
+ const key = `${dir}|${widthMode}|${c.width}|${heightMode}|${c.height}`;
733
+ let size = sizes.get(key);
734
+ if (size === undefined) {
735
+ this._hostLaidAt = null;
736
+ let w = widthMode === 'unconstrained' ? undefined : c.width;
737
+ let h = heightMode === 'unconstrained' ? undefined : c.height;
738
+ if (widthMode === 'at-most') {
739
+ // CSS's fit-content: what it would like, if that fits — and never
740
+ // narrower than what it cannot be narrower than
741
+ const { minContentWidth, maxContentWidth } = this._intrinsicInHost();
742
+ w =
743
+ maxContentWidth <= c.width
744
+ ? undefined
745
+ : Math.max(c.width, minContentWidth);
746
+ }
747
+ if (heightMode === 'at-most') {
748
+ yoga.calculateLayout(w, undefined, dir);
749
+ h = marginBoxHeight(yoga) <= c.height ? undefined : c.height;
750
+ }
751
+ yoga.calculateLayout(w, h, dir);
752
+ size = { width: marginBoxWidth(yoga), height: marginBoxHeight(yoga) };
753
+ remember(sizes, key, size);
754
+ }
755
+ return { width: size.width, height: size.height };
756
+ }
757
+
758
+ /** `LayoutChild.intrinsicSizes`. The maximum is a layout with no bound;
759
+ * the minimum is the content floor measured for this child, and until
760
+ * one has been, the maximum — nothing is squeezed on a guess. */
761
+ _intrinsicInHost() {
762
+ const dir = yogaDirection(this.parent);
763
+ const sizes = this._hostSizesNow();
764
+ let widest = sizes.get(dir);
765
+ if (widest === undefined) {
766
+ const yoga = this.yoga;
767
+ this._hostLaidAt = null;
768
+ yoga.calculateLayout(undefined, undefined, dir);
769
+ widest = {
770
+ max: marginBoxWidth(yoga),
771
+ margins:
772
+ yoga.getComputedMargin(Yoga.EDGE_LEFT) +
773
+ yoga.getComputedMargin(Yoga.EDGE_RIGHT),
774
+ };
775
+ remember(sizes, dir, widest);
776
+ }
777
+ const { max, margins } = widest;
778
+ const min =
779
+ this._floorW === undefined ? max : Math.min(max, this._floorW + margins);
780
+ return { minContentWidth: min, maxContentWidth: max };
781
+ }
782
+
783
+ /** The sizes remembered for this child of a layout host: kept while
784
+ * nothing inside it changes, which dirt on its root would say
785
+ * (`layoutHostChild`), and apart for each side of the pixel grid, since
786
+ * the same layout measured off it comes to a different size. */
787
+ _hostSizesNow() {
788
+ let memo = this._hostSizes;
789
+ if (memo === null || this.yoga.isDirty()) {
790
+ memo = this._hostSizes = { onGrid: new Map(), exact: new Map() };
791
+ }
792
+ return isMeasuringExactly() ? memo.exact : memo.onGrid;
793
+ }
794
+
795
+ /** This child's `layoutItem`, against the options its layout declared. */
796
+ _layoutItemOptions() {
797
+ const host = this.parent?._host;
798
+ if (!host) return NO_OPTIONS;
799
+ const raw = this.style.layoutItem ?? null;
800
+ const cache = this._itemCache;
801
+ if (
802
+ cache !== null &&
803
+ cache.raw === raw &&
804
+ cache.def === host.def &&
805
+ cache.scale === this.scale
806
+ ) {
807
+ return cache.options;
808
+ }
809
+ const { options, problem } = resolveOptions(
810
+ host.def.childOptions,
811
+ raw,
812
+ this.scale,
813
+ `<${this.kind} style={{ layoutItem }}> under layout "${host.name}"`,
814
+ );
815
+ if (problem) reportStyleProblem(this, problem, 'It takes its default');
816
+ this._itemCache = { raw, def: host.def, scale: this.scale, options };
817
+ return options;
818
+ }
819
+ }
820
+
821
+ /** WindowNode's half of layout hosts, installed onto `WindowNode.prototype` by window/window.js. */
822
+ export class WindowLayoutHost {
823
+ // --- layout hosts (docs/styling.md, "Custom layouts") -----------------
824
+
825
+ /**
826
+ * Mark every layout host whose children changed as dirty, before a pass —
827
+ * since nothing else would. A host's children are yoga trees of their own,
828
+ * so their dirt stops at their own roots and never reaches the host's box.
829
+ * (Yoga's dirtied callback is no substitute: it fires only on the way from
830
+ * clean to dirty, and a child never laid out — hidden since it mounted — is
831
+ * dirty already and would never say so.) Deepest first, so a host inside
832
+ * another's child dirties that child's tree before the outer host looks.
833
+ */
834
+ _sweepLayoutHosts() {
835
+ if (this._layoutHosts.size === 0) return;
836
+ for (const host of this._hostsInOrder(true)) {
837
+ for (const child of host.children) {
838
+ if (!child.yoga || child.isWindow || child.hidden) continue;
839
+ if (child.style.display === 'none') continue;
840
+ if (child.yoga.isDirty()) {
841
+ host._markHostDirty();
842
+ break;
843
+ }
844
+ }
845
+ }
846
+ }
847
+
848
+ /** This window's layout hosts, each before the hosts inside its children —
849
+ * or after them, `deepestFirst`. */
850
+ _hostsInOrder(deepestFirst) {
851
+ const hosts = [];
852
+ for (const host of this._layoutHosts) {
853
+ if (host.destroyed || host.root !== this || host._host == null) {
854
+ this._layoutHosts.delete(host);
855
+ continue;
856
+ }
857
+ hosts.push(host);
858
+ }
859
+ if (hosts.length > 1) {
860
+ const depth = new Map();
861
+ for (const host of hosts) {
862
+ let d = 0;
863
+ for (let n = host.parent; n; n = n.parent) if (n._host != null) d++;
864
+ depth.set(host, d);
865
+ }
866
+ hosts.sort((a, b) =>
867
+ deepestFirst
868
+ ? depth.get(b) - depth.get(a)
869
+ : depth.get(a) - depth.get(b),
870
+ );
871
+ }
872
+ return hosts;
873
+ }
874
+
875
+ /**
876
+ * The content floors inside every layout host's children, tree by tree. A
877
+ * card a masonry lays out is a yoga tree of its own, and the window's own
878
+ * measuring pass stops at the host, which answers for itself as a leaf —
879
+ * so without this the rows inside the card would get no floor and squeeze
880
+ * to nothing. Each stale child is measured the way the window measures its
881
+ * tree: no room on offer, the shrink borrowed (`setMeasuringShrink`), the
882
+ * span read back into the extents of everything stale inside it
883
+ * (`contentSpan`), which `_writeFloors` then writes with the rest. The
884
+ * child's own extent is its min-content width, which is what its host
885
+ * reads of it (`LayoutChild.intrinsicSizes`).
886
+ *
887
+ * Deepest hosts first, so a host inside a card has its own children's
888
+ * extents in hand when the card's pass asks it for its minimum.
889
+ */
890
+ _measureHostChildWidths() {
891
+ for (const host of this._hostsInOrder(true)) {
892
+ if (host.hidden || host.style.display === 'none') continue;
893
+ const dir = yogaDirection(host);
894
+ for (const child of host.children) {
895
+ if (!inFlow(child) || child.hidden || child._floorW !== undefined) {
896
+ continue;
897
+ }
898
+ const cy = child.yoga;
899
+ const shrunk = [];
900
+ setMeasuringShrink(child, 'width', shrunk);
901
+ layoutHostChild(child, 0, undefined, dir);
902
+ const span = contentSpan(child, 'width', null, this);
903
+ child._floorW = declaresOwnMinimum(child, 'width')
904
+ ? cy.getComputedWidth()
905
+ : span;
906
+ restoreShrink(shrunk);
907
+ this._floorsMeasured += 1;
908
+ // the floors inside it are written from what this found…
909
+ this._floorsStale.add(child);
910
+ // …and it was laid out with no room, which the placement undoes
911
+ host._host.measured = true;
912
+ }
913
+ }
914
+ }
915
+
916
+ /** Every layout host's final call, parents first: placing a host lays its
917
+ * children's trees out, which is where the hosts inside them are
918
+ * measured — and they place after. True when a host measured height
919
+ * floors inside its children, which is a pass off the pixel grid. */
920
+ _placeLayoutHosts(floors) {
921
+ let measuredHeights = false;
922
+ for (const host of this._hostsInOrder(false)) {
923
+ if (host._placeHostChildren(floors)) measuredHeights = true;
924
+ }
925
+ return measuredHeights;
926
+ }
927
+
928
+ /**
929
+ * The layouts that threw this pass, turned off: each host goes back to
930
+ * being a flex box, and stays one until its style names a different
931
+ * layout. True when there were any — the caller lays out again, so the
932
+ * frame the throw happened in is already the flexbox one.
933
+ */
934
+ _abandonFailedHosts() {
935
+ if (this._failedHosts.size === 0) return false;
936
+ for (const host of [...this._failedHosts]) {
937
+ if (host.destroyed || host._host === null) continue;
938
+ host._layoutAbandoned = {
939
+ def: host._host.def,
940
+ raw: layoutOf(host.style)?.raw ?? null,
941
+ };
942
+ host._leaveHost();
943
+ }
944
+ this._failedHosts.clear();
945
+ this._floorsDirty = true;
946
+ this._floorsContentDirty = true;
947
+ return true;
948
+ }
949
+ }