react-x11 2.11.0 → 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.
Files changed (96) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +292 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/threaded.js +246 -0
  20. package/src/cocoa/window.js +256 -42
  21. package/src/components/Select.js +2 -2
  22. package/src/components/anchor.js +3 -3
  23. package/src/components/native.js +12 -7
  24. package/src/components/theme.js +2 -2
  25. package/src/debug.js +1 -1
  26. package/src/decorations.js +1 -1
  27. package/src/editmenu.js +2 -2
  28. package/src/errors.js +46 -0
  29. package/src/events.js +6 -6
  30. package/src/foreignnodes.js +3 -2
  31. package/src/frames.js +2 -2
  32. package/src/glnodes.js +1 -1
  33. package/src/grid.js +1653 -0
  34. package/src/host.d.ts +230 -0
  35. package/src/host.js +11 -3
  36. package/src/imagesource.js +1 -1
  37. package/src/index.d.ts +21 -4
  38. package/src/index.js +9 -1
  39. package/src/layouts.js +721 -0
  40. package/src/node.d.ts +4 -2
  41. package/src/node.js +19 -21
  42. package/src/nodes/animation.js +644 -0
  43. package/src/nodes/box.js +21 -0
  44. package/src/nodes/boxpaint.js +473 -0
  45. package/src/nodes/canvas.js +269 -0
  46. package/src/nodes/cascade.js +600 -0
  47. package/src/nodes/damage.js +183 -0
  48. package/src/nodes/edithistory.js +124 -0
  49. package/src/nodes/editmenupopup.js +260 -0
  50. package/src/nodes/hittest.js +185 -0
  51. package/src/nodes/image.js +266 -0
  52. package/src/nodes/install.js +75 -0
  53. package/src/nodes/invalidate.js +465 -0
  54. package/src/nodes/kinds.js +31 -0
  55. package/src/nodes/layout.js +439 -0
  56. package/src/nodes/layouthost.js +949 -0
  57. package/src/nodes/node.js +868 -0
  58. package/src/nodes/paint.js +466 -0
  59. package/src/nodes/position.js +366 -0
  60. package/src/nodes/preedit.js +127 -0
  61. package/src/nodes/queries.js +330 -0
  62. package/src/nodes/rects.js +102 -0
  63. package/src/nodes/scrollable.js +891 -0
  64. package/src/nodes/scrollbars.js +138 -0
  65. package/src/nodes/scrollblit.js +1034 -0
  66. package/src/nodes/selectable.js +142 -0
  67. package/src/nodes/styling.js +225 -0
  68. package/src/nodes/text.js +649 -0
  69. package/src/nodes/textarea.js +391 -0
  70. package/src/nodes/textinput.js +1146 -0
  71. package/src/nodes/util.js +17 -0
  72. package/src/nodes/window/anchoring.js +161 -0
  73. package/src/nodes/window/capabilities.js +190 -0
  74. package/src/nodes/window/debugpaint.js +83 -0
  75. package/src/nodes/window/droptarget.js +145 -0
  76. package/src/nodes/window/floors.js +577 -0
  77. package/src/nodes/window/flush.js +334 -0
  78. package/src/nodes/window/hints.js +482 -0
  79. package/src/nodes/window/listeners.js +222 -0
  80. package/src/nodes/window/popup.js +71 -0
  81. package/src/nodes/window/size.js +591 -0
  82. package/src/nodes/window/window.js +945 -0
  83. package/src/palette.js +1 -1
  84. package/src/registry.js +7 -3
  85. package/src/styles.js +137 -15
  86. package/src/svgnodes.js +2 -1
  87. package/src/testing/harness.js +2 -2
  88. package/src/textselection.js +5 -3
  89. package/src/trace-registry.js +1 -1
  90. package/src/types/components.d.ts +38 -6
  91. package/src/types/elements.d.ts +11 -1
  92. package/src/types/nodes.d.ts +17 -2
  93. package/src/types/style.d.ts +94 -3
  94. package/src/windowstate.js +1 -1
  95. package/src/yoga.js +1 -1
  96. package/src/nodes.js +0 -13120
package/src/layouts.js ADDED
@@ -0,0 +1,721 @@
1
+ // Layout algorithms and positions from outside the renderer — the two seams
2
+ // docs/architecture/custom-layout.md is the design record for.
3
+ //
4
+ // A **layout** arranges a box's children. It is asked how big the box is for
5
+ // the room on offer, and then where each child goes in the room the box got.
6
+ // It runs inside the pass that lays the window out — each child is a yoga
7
+ // tree of its own, and the box is a measured leaf in its parent's — so what it
8
+ // decides is on screen in the frame that asked. Nothing React-side can get
9
+ // there: `onLayout` hears about a pass after it painted, and a
10
+ // `useLayoutEffect` runs before the pass it would need to read.
11
+ //
12
+ // A **position** is CSS's positioning scheme, opened up: a node laid out in
13
+ // flow and then moved, by an offset its scheme works out from what the pass
14
+ // produced, resizing nothing. `sticky` is exactly that, and is written here
15
+ // against the same context a registered position is handed — which is what
16
+ // keeps the seam honest: the built-in one is expressed with it, so the next
17
+ // one can be. The insets are the scheme's to read (thresholds, for sticky),
18
+ // never offsets.
19
+ //
20
+ // Both are looked up by name from a style property (`layout`, `position`),
21
+ // so a container query block can swap one in the frame its container crosses
22
+ // a threshold. Both are plain data, so this module needs nothing from
23
+ // src/nodes/ — the node side is there, next to the pass it runs in.
24
+
25
+ import { gridLayout } from './grid.js';
26
+
27
+ /** name -> definition, insertion-ordered: the order an error lists them in. */
28
+ const layouts = new Map();
29
+ const positions = new Map();
30
+
31
+ // The re-registration rule registry.js has for elements (issue #318): under
32
+ // an active hot-reload session a duplicate is the previous version of the
33
+ // same module, and replaces silently. registry.js owns the flag — it is the
34
+ // one the refresh loader flips — and forwards it here.
35
+ let hotReloadSession = false;
36
+
37
+ /** @internal — forwarded by registry.js's `markHotReloadSession`. */
38
+ export function markLayoutsHotReloadSession(active = true) {
39
+ hotReloadSession = active;
40
+ }
41
+
42
+ const BUILTIN = Symbol('react-x11 built-in');
43
+
44
+ const NAME = /^[A-Za-z_][\w-]*$/;
45
+
46
+ function checkName(name, fn) {
47
+ if (typeof name !== 'string' || !NAME.test(name)) {
48
+ throw new Error(
49
+ `react-x11: ${fn}(${JSON.stringify(name)}) — a name is letters, ` +
50
+ "digits, '_' and '-', not starting with a digit, the way a style " +
51
+ "writes it: layout: 'masonry'.",
52
+ );
53
+ }
54
+ }
55
+
56
+ function checkDuplicate(table, name, fn, definition, key) {
57
+ const existing = table.get(name);
58
+ if (!existing || definition.override) return;
59
+ // the same function arriving twice is one definition evaluated twice, and
60
+ // never a conflict; under hot reload any duplicate is the module's own
61
+ if (existing[key] === definition[key] || hotReloadSession) return;
62
+ throw new Error(
63
+ `react-x11: ${fn}("${name}") — "${name}" is already registered` +
64
+ (existing.builtin ? ', and is built in' : '') +
65
+ '. Pass { override: true } if replacing it is deliberate. (Under hot ' +
66
+ 'reload a module re-registering its own replaces it silently; this ' +
67
+ 'means two different definitions collided outside any reload.)',
68
+ );
69
+ }
70
+
71
+ // --- options ------------------------------------------------------------------
72
+
73
+ /**
74
+ * What an option may be. A `length` is written in logical pixels, like every
75
+ * length in a style, and handed to the algorithm in device pixels — the unit
76
+ * its constraints and its children's sizes are in — so an algorithm never
77
+ * multiplies by the scale itself and never forgets to (docs/scale.md).
78
+ */
79
+ const OPTION_TYPES = [
80
+ 'length',
81
+ 'number',
82
+ 'integer',
83
+ 'boolean',
84
+ 'string',
85
+ 'any',
86
+ ];
87
+
88
+ const EMPTY_SCHEMA = Object.freeze({});
89
+
90
+ function fits(type, value) {
91
+ if (Array.isArray(type)) return type.includes(value);
92
+ switch (type) {
93
+ case 'length':
94
+ case 'number':
95
+ return typeof value === 'number' && Number.isFinite(value);
96
+ case 'integer':
97
+ return Number.isInteger(value);
98
+ case 'boolean':
99
+ return typeof value === 'boolean';
100
+ case 'string':
101
+ return typeof value === 'string';
102
+ default:
103
+ return true;
104
+ }
105
+ }
106
+
107
+ const describeType = (type) =>
108
+ Array.isArray(type)
109
+ ? `one of ${type.map((v) => JSON.stringify(v)).join(', ')}`
110
+ : type === 'integer'
111
+ ? 'an integer'
112
+ : type === 'any'
113
+ ? 'anything'
114
+ : `a ${type}`;
115
+
116
+ function checkSchema(schema, what) {
117
+ if (schema === undefined) return EMPTY_SCHEMA;
118
+ if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
119
+ throw new Error(
120
+ `react-x11: ${what} — options are declared as an object, ` +
121
+ "{ gap: { type: 'length', default: 8 } }.",
122
+ );
123
+ }
124
+ for (const key of Object.keys(schema)) {
125
+ if (key === 'name') {
126
+ throw new Error(
127
+ `react-x11: ${what} declares an option called "name" — that key is ` +
128
+ 'the style naming the algorithm, and cannot be an option too.',
129
+ );
130
+ }
131
+ const spec = schema[key];
132
+ const type = spec?.type;
133
+ const ok = Array.isArray(type)
134
+ ? type.length > 0
135
+ : OPTION_TYPES.includes(type);
136
+ if (!ok) {
137
+ throw new Error(
138
+ `react-x11: ${what} option "${key}" has type ${JSON.stringify(type)} ` +
139
+ `— expected one of ${OPTION_TYPES.map((t) => `'${t}'`).join(', ')}, ` +
140
+ 'or an array of the values it may take.',
141
+ );
142
+ }
143
+ if (spec.default != null && !fits(type, spec.default)) {
144
+ throw new Error(
145
+ `react-x11: ${what} option "${key}" defaults to ` +
146
+ `${JSON.stringify(spec.default)}, which is not ${describeType(type)}.`,
147
+ );
148
+ }
149
+ }
150
+ return schema;
151
+ }
152
+
153
+ /**
154
+ * The options a style wrote, against the schema that declared them: every
155
+ * declared one present (its default when left out), lengths in device
156
+ * pixels. A value of the wrong type takes the default and is described in
157
+ * `problem`, for the caller to report the way a bad `$token` is reported —
158
+ * one style property is not worth the whole GUI.
159
+ */
160
+ export function resolveOptions(schema, raw, scale, where) {
161
+ const options = {};
162
+ let problem = null;
163
+ for (const key of Object.keys(schema)) {
164
+ const spec = schema[key];
165
+ let value = raw?.[key];
166
+ if (value == null) value = spec.default;
167
+ else if (!fits(spec.type, value)) {
168
+ problem ??=
169
+ `react-x11: ${where} — option "${key}" is ${JSON.stringify(value)}, ` +
170
+ `where it takes ${describeType(spec.type)}`;
171
+ value = spec.default;
172
+ }
173
+ if (spec.type === 'length' && typeof value === 'number') value *= scale;
174
+ options[key] = value;
175
+ }
176
+ if (raw) {
177
+ for (const key of Object.keys(raw)) {
178
+ if (key === 'name' || Object.hasOwn(schema, key)) continue;
179
+ const names = Object.keys(schema).map((n) => `"${n}"`);
180
+ problem ??=
181
+ `react-x11: ${where} — unknown option "${key}" (` +
182
+ (names.length ? `it takes ${names.join(', ')}` : 'it takes none') +
183
+ ')';
184
+ }
185
+ }
186
+ return { options, problem };
187
+ }
188
+
189
+ // --- what a style asks for ----------------------------------------------------
190
+
191
+ /**
192
+ * A `layout` or `position` value, read: the name, the definition it names
193
+ * (null when nothing by that name is registered) and the options written
194
+ * beside it. A string is a name with nothing else to say.
195
+ */
196
+ function request(value, table) {
197
+ if (value == null || value === false) return null;
198
+ const raw = typeof value === 'object' ? value : null;
199
+ const name = raw ? raw.name : value;
200
+ return { name, def: table.get(name) ?? null, raw };
201
+ }
202
+
203
+ /**
204
+ * The layout a resolved style asks its node to arrange its children with —
205
+ * named by `layout`, or by `display: 'grid'`, which is CSS's name for the
206
+ * built-in grid and the same request as `layout: 'grid'`. Where the two
207
+ * disagree — `display: 'grid'` beside another layout, or `layout: 'grid'`
208
+ * beside `display: 'flex'` — the answer carries a `conflict` for the node to
209
+ * report and no definition, so the box is flexbox until they agree.
210
+ * `display: 'flex'` beside any other layout is no disagreement: it says the
211
+ * box is shown, which is all `display` said before grid was one of its values.
212
+ */
213
+ export function layoutOf(style) {
214
+ const requested = request(style.layout, layouts);
215
+ const display = style.display;
216
+ if (display === 'grid') {
217
+ if (requested === null) return GRID_REQUEST;
218
+ if (requested.name === 'grid') return requested;
219
+ return disagreement("display: 'grid'", requested);
220
+ }
221
+ if (display === 'flex' && requested?.name === 'grid') {
222
+ return disagreement("display: 'flex'", requested);
223
+ }
224
+ return requested;
225
+ }
226
+
227
+ function disagreement(display, requested) {
228
+ return {
229
+ name: requested.name,
230
+ def: null,
231
+ raw: requested.raw,
232
+ conflict:
233
+ `react-x11: ${display} and layout: ${JSON.stringify(requested.name)} ` +
234
+ 'disagree about how this box lays out its children — say it once ' +
235
+ "(display: 'grid' and layout: 'grid' are the same request)",
236
+ };
237
+ }
238
+
239
+ /** CSS's own positions, which yoga lays out — and the one CSS has that this
240
+ * renderer does not (`fixed`), kept out of the registry so it cannot come
241
+ * to mean something else. `sticky` is the built-in placed one. */
242
+ const CSS_POSITIONS = ['static', 'relative', 'absolute', 'fixed', 'sticky'];
243
+
244
+ /**
245
+ * Whether a style's `position` is placed after layout rather than by it —
246
+ * `sticky`, or anything registered. The cheap test the style funnel and the
247
+ * paint order run on every node.
248
+ */
249
+ export const isPlaced = (style) => {
250
+ const p = style.position;
251
+ return p != null && p !== 'static' && p !== 'relative' && p !== 'absolute';
252
+ };
253
+
254
+ /** The placed position a resolved style asks for, or null for one yoga
255
+ * lays out by itself. */
256
+ export function positionOf(style) {
257
+ if (!isPlaced(style)) return null;
258
+ if (style.position === 'sticky') return STICKY_REQUEST;
259
+ return request(style.position, positions);
260
+ }
261
+
262
+ const quoted = (names) => names.map((n) => JSON.stringify(n)).join(', ');
263
+
264
+ export function unknownLayoutMessage(name) {
265
+ return (
266
+ `react-x11: layout ${JSON.stringify(name)} is not registered ` +
267
+ `(registered: ${quoted([...layouts.keys()])}) — registerLayout() from ` +
268
+ 'react-x11/host, before the tree that uses it renders. See ' +
269
+ 'docs/extending.md, "A layout algorithm of your own"'
270
+ );
271
+ }
272
+
273
+ export function unknownPositionMessage(name) {
274
+ if (name === 'fixed') {
275
+ return (
276
+ "react-x11: position: 'fixed' is not supported — a <popup> is a " +
277
+ "window of its own, above this one; or position: 'absolute' inside a " +
278
+ 'box that fills the window'
279
+ );
280
+ }
281
+ const known = [...positions.keys()];
282
+ return (
283
+ `react-x11: position ${JSON.stringify(name)} is neither CSS's ` +
284
+ "('static', 'relative', 'absolute', 'sticky') nor registered" +
285
+ (known.length ? ` (registered: ${quoted(known)})` : '') +
286
+ ' — registerPosition() from react-x11/host, before the tree that uses ' +
287
+ 'it renders. See docs/extending.md, "A position of your own"'
288
+ );
289
+ }
290
+
291
+ // --- registration -------------------------------------------------------------
292
+
293
+ /**
294
+ * Teach react-x11 a layout algorithm, for any box to use with
295
+ * `style={{ layout: name }}` (docs/extending.md, "A layout algorithm of your
296
+ * own").
297
+ *
298
+ * `layout(children, constraints, options, info)` answers two questions with
299
+ * one function: how big the box's content is for `constraints` — asked
300
+ * several times per pass, at sizes nothing is drawn at — and, when both
301
+ * modes are `'exactly'`, where each child goes, as one rect per child. It
302
+ * must be a pure function of its arguments.
303
+ */
304
+ export function registerLayout(name, definition) {
305
+ checkName(name, 'registerLayout');
306
+ if (!definition || typeof definition.layout !== 'function') {
307
+ throw new Error(
308
+ `react-x11: registerLayout("${name}") needs a layout(children, ` +
309
+ 'constraints, options, info) function that returns { width, height, ' +
310
+ 'children }.',
311
+ );
312
+ }
313
+ checkDuplicate(layouts, name, 'registerLayout', definition, 'layout');
314
+ layouts.set(
315
+ name,
316
+ Object.freeze({
317
+ name,
318
+ layout: definition.layout,
319
+ options: checkSchema(definition.options, `layout "${name}"`),
320
+ childOptions: checkSchema(
321
+ definition.childOptions,
322
+ `layout "${name}"'s childOptions`,
323
+ ),
324
+ builtin: definition[BUILTIN] === true,
325
+ }),
326
+ );
327
+ }
328
+
329
+ /** Undo a registration; true if there was one. The built-in layouts stay. */
330
+ export function unregisterLayout(name) {
331
+ if (layouts.get(name)?.builtin) return false;
332
+ return layouts.delete(name);
333
+ }
334
+
335
+ /** Registered layout names, the built-in ones first. */
336
+ export function registeredLayouts() {
337
+ return [...layouts.keys()];
338
+ }
339
+
340
+ /** The definition registered under `name`, or undefined. */
341
+ export function layoutDefinition(name) {
342
+ return layouts.get(name);
343
+ }
344
+
345
+ /**
346
+ * Teach react-x11 a positioning scheme, for any node to use with
347
+ * `style={{ position: name }}` (docs/extending.md, "A position of your own").
348
+ *
349
+ * The node is laid out in flow, as `relative` is, and then
350
+ * `place(node, context)` runs — after every layout pass, the pass a scroll
351
+ * runs included — returning how far to move it from where layout put it,
352
+ * `{ x, y }` in device pixels, or null to leave it there. It may not resize
353
+ * anything, which is what makes it safe to run that often.
354
+ */
355
+ export function registerPosition(name, definition) {
356
+ checkName(name, 'registerPosition');
357
+ if (CSS_POSITIONS.includes(name)) {
358
+ throw new Error(
359
+ `react-x11: registerPosition("${name}") — "${name}" is one of CSS's ` +
360
+ 'own positions, and not a name a registered one can take.',
361
+ );
362
+ }
363
+ if (!definition || typeof definition.place !== 'function') {
364
+ throw new Error(
365
+ `react-x11: registerPosition("${name}") needs a place(node, context) ` +
366
+ 'function that returns the offset { x, y } to move the node by, or ' +
367
+ 'null to leave it where layout put it.',
368
+ );
369
+ }
370
+ checkDuplicate(positions, name, 'registerPosition', definition, 'place');
371
+ positions.set(
372
+ name,
373
+ Object.freeze({
374
+ name,
375
+ place: definition.place,
376
+ options: checkSchema(definition.options, `position "${name}"`),
377
+ }),
378
+ );
379
+ }
380
+
381
+ /** Undo a registration; true if there was one. */
382
+ export function unregisterPosition(name) {
383
+ return positions.delete(name);
384
+ }
385
+
386
+ /** Registered position names, in registration order. */
387
+ export function registeredPositions() {
388
+ return [...positions.keys()];
389
+ }
390
+
391
+ // --- what an algorithm returns ------------------------------------------------
392
+
393
+ const isExtent = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
394
+
395
+ const describe = (v) =>
396
+ v === undefined
397
+ ? 'undefined'
398
+ : typeof v === 'object' && v !== null
399
+ ? JSON.stringify(v)
400
+ : String(v);
401
+
402
+ /**
403
+ * Check what a layout answered, so a mistake is named where it was made —
404
+ * left to itself a `NaN` spreads through every ancestor's rect, and a
405
+ * missing rect puts a child at the window origin with nothing saying why.
406
+ * The rects are only required of the final call, the one that places.
407
+ */
408
+ export function checkLayoutResult(result, count, final, name) {
409
+ if (result === null || typeof result !== 'object') {
410
+ throw new TypeError(
411
+ `layout "${name}" returned ${describe(result)} — it must return ` +
412
+ '{ width, height, children }',
413
+ );
414
+ }
415
+ if (!isExtent(result.width) || !isExtent(result.height)) {
416
+ throw new TypeError(
417
+ `layout "${name}" returned { width: ${describe(result.width)}, ` +
418
+ `height: ${describe(result.height)} } — both have to be finite ` +
419
+ 'numbers, 0 or more',
420
+ );
421
+ }
422
+ if (!final) return result;
423
+ const rects = result.children;
424
+ if (!Array.isArray(rects) || rects.length !== count) {
425
+ throw new TypeError(
426
+ `layout "${name}" has to return one rect per child when it places ` +
427
+ `(${count} children), and returned ` +
428
+ (Array.isArray(rects) ? `${rects.length}` : describe(rects)),
429
+ );
430
+ }
431
+ for (let i = 0; i < rects.length; i++) {
432
+ const r = rects[i];
433
+ if (
434
+ !r ||
435
+ !Number.isFinite(r.x) ||
436
+ !Number.isFinite(r.y) ||
437
+ (r.width != null && !isExtent(r.width)) ||
438
+ (r.height != null && !isExtent(r.height))
439
+ ) {
440
+ throw new TypeError(
441
+ `layout "${name}" placed child ${i} at ${describe(r)} — a rect is ` +
442
+ '{ x, y } with an optional width and height, all finite',
443
+ );
444
+ }
445
+ }
446
+ return result;
447
+ }
448
+
449
+ // --- position: 'sticky' -------------------------------------------------------
450
+
451
+ /**
452
+ * A sticky inset as a distance in device pixels, or null for an edge that
453
+ * does not stick. Numbers arrive already scaled (styles.js,
454
+ * `scaleResolvedStyle`); a percentage is of `size`, the pane's scrollport
455
+ * along that axis, which is CSS's rule for a sticky inset. `'auto'` and
456
+ * anything else leave the edge free, as `auto` does in CSS.
457
+ */
458
+ function stickyInset(value, size) {
459
+ if (typeof value === 'number') return value;
460
+ if (typeof value === 'string' && value.endsWith('%')) {
461
+ const percent = Number.parseFloat(value);
462
+ return Number.isFinite(percent) ? (percent / 100) * size : null;
463
+ }
464
+ return null;
465
+ }
466
+
467
+ /**
468
+ * CSS's sticky offset, per axis. An edge with an inset may not cross the
469
+ * pane's matching edge moved in by that inset, so the node is pushed back
470
+ * inside — but never so far that its margin box leaves its parent's content
471
+ * box, which is what carries a section's header off with the section. Where
472
+ * both edges of an axis stick, the top and left pushes are applied last and
473
+ * win, as in Blink.
474
+ *
475
+ * The pane's edges are its scrollport, inside the border and over the
476
+ * padding: the band the content scrolls through. With no pane above, the
477
+ * node stays where layout put it — `overflow: 'hidden'` clips without being
478
+ * one, which is CSS's `clip` rather than its `hidden`.
479
+ */
480
+ function placeSticky(node, { laidOut, pane, container, margin, direction }) {
481
+ if (!pane) return null;
482
+ const style = node.style;
483
+ const port = pane.scrollport;
484
+ const portWidth = port.right - port.left;
485
+ const portHeight = port.bottom - port.top;
486
+ // yoga's precedence, which the inset appliers use too: the logical edge
487
+ // wins over the physical one it lands on
488
+ const rtl = direction === 'rtl';
489
+ const leftInset = rtl
490
+ ? (style.end ?? style.left)
491
+ : (style.start ?? style.left);
492
+ const rightInset = rtl
493
+ ? (style.start ?? style.right)
494
+ : (style.end ?? style.right);
495
+ const top = stickyInset(style.top, portHeight);
496
+ const bottom = stickyInset(style.bottom, portHeight);
497
+ const left = stickyInset(leftInset, portWidth);
498
+ const right = stickyInset(rightInset, portWidth);
499
+ if (top === null && bottom === null && left === null && right === null) {
500
+ return null;
501
+ }
502
+ // the margin box has to stay inside the container
503
+ const box = {
504
+ left: container.left + margin.left,
505
+ top: container.top + margin.top,
506
+ right: container.right - margin.right,
507
+ bottom: container.bottom - margin.bottom,
508
+ };
509
+ const { x: atX, y: atY, width, height } = laidOut;
510
+ let x = 0;
511
+ let y = 0;
512
+ if (right !== null) {
513
+ const push = Math.min(0, port.right - right - (atX + width));
514
+ x += Math.max(push, Math.min(0, box.left - atX));
515
+ }
516
+ if (left !== null) {
517
+ const push = Math.max(0, port.left + left - atX);
518
+ x += Math.min(push, Math.max(0, box.right - (atX + width)));
519
+ }
520
+ if (bottom !== null) {
521
+ const push = Math.min(0, port.bottom - bottom - (atY + height));
522
+ y += Math.max(push, Math.min(0, box.top - atY));
523
+ }
524
+ if (top !== null) {
525
+ const push = Math.max(0, port.top + top - atY);
526
+ y += Math.min(push, Math.max(0, box.bottom - (atY + height)));
527
+ }
528
+ return { x, y };
529
+ }
530
+
531
+ const STICKY = Object.freeze({
532
+ name: 'sticky',
533
+ place: placeSticky,
534
+ options: EMPTY_SCHEMA,
535
+ });
536
+ const STICKY_REQUEST = Object.freeze({
537
+ name: 'sticky',
538
+ def: STICKY,
539
+ raw: null,
540
+ });
541
+
542
+ // --- the built-in layouts -----------------------------------------------------
543
+
544
+ /**
545
+ * `masonry`: columns, each child dropped into whichever is shortest so far —
546
+ * the layout a photo wall or a board of notes of different heights wants,
547
+ * and one flexbox cannot write: a wrapping row lines its items up in rows,
548
+ * and a wrapping column needs a height to wrap at.
549
+ *
550
+ * The number of columns is `columns`, or as many `columnWidth`-wide columns
551
+ * as the width fits; they share the width equally. Gaps come from the box's
552
+ * own `columnGap` / `rowGap` / `gap`, the properties a flex box spaces its
553
+ * children with. A child's `layoutItem: { span }` lays it across that many
554
+ * columns. Heights are the children's own at the column width.
555
+ */
556
+ function masonry(children, c, { columns, columnWidth }, { style }) {
557
+ const colGap = style.columnGap ?? style.gap ?? 0;
558
+ const rowGap = style.rowGap ?? style.gap ?? 0;
559
+ let cols;
560
+ let colW;
561
+ if (c.widthMode === 'unconstrained') {
562
+ cols = Math.max(1, columns ?? 1);
563
+ colW = columnWidth;
564
+ } else {
565
+ cols =
566
+ columns != null
567
+ ? Math.max(1, columns)
568
+ : Math.max(1, Math.floor((c.width + colGap) / (columnWidth + colGap)));
569
+ colW = Math.max(0, (c.width - colGap * (cols - 1)) / cols);
570
+ if (c.widthMode === 'at-most') {
571
+ // The question the content floors ask (`at-most 0`): a column is never
572
+ // narrower than the widest thing it has to hold.
573
+ for (const child of children) {
574
+ colW = Math.max(colW, child.intrinsicSizes().minContentWidth);
575
+ }
576
+ }
577
+ }
578
+ const tops = new Array(cols).fill(0);
579
+ const rects = [];
580
+ for (const child of children) {
581
+ const span = Math.min(cols, Math.max(1, child.options.span ?? 1));
582
+ // the run of `span` columns whose tallest is shortest, leftmost on a tie
583
+ let best = 0;
584
+ let bestTop = Infinity;
585
+ for (let j = 0; j + span <= cols; j++) {
586
+ let top = 0;
587
+ for (let k = j; k < j + span; k++) top = Math.max(top, tops[k]);
588
+ if (top < bestTop) {
589
+ bestTop = top;
590
+ best = j;
591
+ }
592
+ }
593
+ const width = colW * span + colGap * (span - 1);
594
+ const { height } = child.measure({ width });
595
+ rects.push({ x: best * (colW + colGap), y: bestTop, width });
596
+ for (let k = best; k < best + span; k++) {
597
+ tops[k] = bestTop + height + rowGap;
598
+ }
599
+ }
600
+ const height = rects.length ? Math.max(...tops) - rowGap : 0;
601
+ return {
602
+ width:
603
+ c.widthMode === 'exactly' ? c.width : cols * colW + colGap * (cols - 1),
604
+ height: c.heightMode === 'exactly' ? c.height : Math.max(0, height),
605
+ children: rects,
606
+ };
607
+ }
608
+
609
+ /**
610
+ * `equal-row`: a row whose children are all as wide as the widest of them —
611
+ * a dialog's buttons, a segmented control. Flexbox can make siblings equal
612
+ * only by dividing a width it was given (`flex: 1`); a row that takes its
613
+ * size from its content, which is what a row of buttons at the end of a
614
+ * dialog is, has no width to divide.
615
+ *
616
+ * Squeezed below that, the cells shrink together, down to the widest
617
+ * child's min-content width and no further. Spacing and alignment are the
618
+ * box's own `columnGap`/`gap`, `justifyContent` and `alignItems`, which mean
619
+ * what they mean on a flex row: `alignItems` defaults to `'stretch'`, so the
620
+ * buttons are one height too.
621
+ */
622
+ function equalRow(children, c, _options, { style }) {
623
+ const gap = style.columnGap ?? style.gap ?? 0;
624
+ const n = children.length;
625
+ if (n === 0) {
626
+ return {
627
+ width: c.widthMode === 'exactly' ? c.width : 0,
628
+ height: c.heightMode === 'exactly' ? c.height : 0,
629
+ children: [],
630
+ };
631
+ }
632
+ const gaps = gap * (n - 1);
633
+ let cell = 0;
634
+ for (const child of children) {
635
+ cell = Math.max(cell, child.intrinsicSizes().maxContentWidth);
636
+ }
637
+ if (c.widthMode !== 'unconstrained') {
638
+ const fit = (c.width - gaps) / n;
639
+ if (fit < cell) {
640
+ let floor = 0;
641
+ for (const child of children) {
642
+ floor = Math.max(floor, child.intrinsicSizes().minContentWidth);
643
+ }
644
+ cell = Math.max(fit, floor);
645
+ }
646
+ }
647
+ const heights = children.map(
648
+ (child) => child.measure({ width: cell }).height,
649
+ );
650
+ const used = cell * n + gaps;
651
+ const width = c.widthMode === 'exactly' ? c.width : used;
652
+ const height =
653
+ c.heightMode === 'exactly' ? c.height : Math.max(0, ...heights);
654
+ const free = Math.max(0, width - used);
655
+ let start = 0;
656
+ let step = cell + gap;
657
+ switch (style.justifyContent) {
658
+ case 'center':
659
+ start = free / 2;
660
+ break;
661
+ case 'flex-end':
662
+ start = free;
663
+ break;
664
+ case 'space-between':
665
+ if (n > 1) step += free / (n - 1);
666
+ break;
667
+ case 'space-around':
668
+ start = free / (2 * n);
669
+ step += free / n;
670
+ break;
671
+ case 'space-evenly':
672
+ start = free / (n + 1);
673
+ step += free / (n + 1);
674
+ break;
675
+ default:
676
+ break;
677
+ }
678
+ const align = style.alignItems ?? 'stretch';
679
+ const rects = children.map((_, i) => {
680
+ const x = start + i * step;
681
+ if (align === 'stretch') return { x, y: 0, width: cell, height };
682
+ const h = heights[i];
683
+ const y =
684
+ align === 'center'
685
+ ? (height - h) / 2
686
+ : align === 'flex-end'
687
+ ? height - h
688
+ : 0;
689
+ return { x, y, width: cell };
690
+ });
691
+ return { width, height, children: rects };
692
+ }
693
+
694
+ registerLayout('masonry', {
695
+ [BUILTIN]: true,
696
+ options: {
697
+ // a fixed number of columns; left out, as many as `columnWidth` fits
698
+ columns: { type: 'integer' },
699
+ // the narrowest a column may be when the count is left to the width
700
+ columnWidth: { type: 'length', default: 240 },
701
+ },
702
+ childOptions: {
703
+ // how many columns this child is laid across
704
+ span: { type: 'integer', default: 1 },
705
+ },
706
+ layout: masonry,
707
+ });
708
+
709
+ registerLayout('equal-row', { [BUILTIN]: true, layout: equalRow });
710
+
711
+ // `grid`: CSS's, from a module of its own (src/grid.js). It takes no options:
712
+ // its tracks are the box's own style — `gridTemplateColumns` and the rest —
713
+ // and a child's place is its own `gridColumn`, `gridRow` or `gridArea`.
714
+ registerLayout('grid', { [BUILTIN]: true, layout: gridLayout });
715
+
716
+ /** What `display: 'grid'` asks for, with no `layout` beside it. */
717
+ const GRID_REQUEST = Object.freeze({
718
+ name: 'grid',
719
+ def: layouts.get('grid'),
720
+ raw: null,
721
+ });