react-x11 2.10.2 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- 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
|
+
}
|