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.
Files changed (100) 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 +304 -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/screencolor.js +62 -0
  20. package/src/cocoa/threaded.js +246 -0
  21. package/src/cocoa/window.js +256 -42
  22. package/src/components/Select.js +2 -2
  23. package/src/components/anchor.js +3 -3
  24. package/src/components/native.js +12 -7
  25. package/src/components/theme.js +2 -2
  26. package/src/debug.js +1 -1
  27. package/src/decorations.js +1 -1
  28. package/src/editmenu.js +2 -2
  29. package/src/errors.js +46 -0
  30. package/src/events.js +6 -6
  31. package/src/foreignnodes.js +3 -2
  32. package/src/frames.js +2 -2
  33. package/src/glnodes.js +1 -1
  34. package/src/grid.js +1653 -0
  35. package/src/host.d.ts +230 -0
  36. package/src/host.js +11 -3
  37. package/src/imagesource.js +1 -1
  38. package/src/index.d.ts +21 -4
  39. package/src/index.js +9 -1
  40. package/src/layouts.js +721 -0
  41. package/src/node.d.ts +4 -2
  42. package/src/node.js +19 -21
  43. package/src/nodes/animation.js +644 -0
  44. package/src/nodes/box.js +21 -0
  45. package/src/nodes/boxpaint.js +473 -0
  46. package/src/nodes/canvas.js +269 -0
  47. package/src/nodes/cascade.js +600 -0
  48. package/src/nodes/damage.js +183 -0
  49. package/src/nodes/edithistory.js +124 -0
  50. package/src/nodes/editmenupopup.js +260 -0
  51. package/src/nodes/hittest.js +185 -0
  52. package/src/nodes/image.js +266 -0
  53. package/src/nodes/install.js +75 -0
  54. package/src/nodes/invalidate.js +465 -0
  55. package/src/nodes/kinds.js +31 -0
  56. package/src/nodes/layout.js +439 -0
  57. package/src/nodes/layouthost.js +949 -0
  58. package/src/nodes/node.js +868 -0
  59. package/src/nodes/paint.js +466 -0
  60. package/src/nodes/position.js +366 -0
  61. package/src/nodes/preedit.js +127 -0
  62. package/src/nodes/queries.js +330 -0
  63. package/src/nodes/rects.js +102 -0
  64. package/src/nodes/scrollable.js +891 -0
  65. package/src/nodes/scrollbars.js +138 -0
  66. package/src/nodes/scrollblit.js +1034 -0
  67. package/src/nodes/selectable.js +142 -0
  68. package/src/nodes/styling.js +225 -0
  69. package/src/nodes/text.js +649 -0
  70. package/src/nodes/textarea.js +391 -0
  71. package/src/nodes/textinput.js +1146 -0
  72. package/src/nodes/util.js +17 -0
  73. package/src/nodes/window/anchoring.js +161 -0
  74. package/src/nodes/window/capabilities.js +190 -0
  75. package/src/nodes/window/debugpaint.js +83 -0
  76. package/src/nodes/window/droptarget.js +145 -0
  77. package/src/nodes/window/floors.js +577 -0
  78. package/src/nodes/window/flush.js +334 -0
  79. package/src/nodes/window/hints.js +482 -0
  80. package/src/nodes/window/listeners.js +222 -0
  81. package/src/nodes/window/popup.js +71 -0
  82. package/src/nodes/window/size.js +591 -0
  83. package/src/nodes/window/window.js +945 -0
  84. package/src/palette.js +1 -1
  85. package/src/registry.js +7 -3
  86. package/src/screencolor.js +212 -38
  87. package/src/screencolorhooks.js +6 -2
  88. package/src/styles.js +137 -15
  89. package/src/svgnodes.js +2 -1
  90. package/src/testing/harness.js +2 -2
  91. package/src/textselection.js +5 -3
  92. package/src/trace-registry.js +1 -1
  93. package/src/types/components.d.ts +38 -6
  94. package/src/types/elements.d.ts +11 -1
  95. package/src/types/nodes.d.ts +33 -5
  96. package/src/types/screencolor.d.ts +20 -14
  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,439 @@
1
+ // Layout: yoga's measure seam for a node's content, and the walk that turns
2
+ // yoga's offsets into absolute rects each pass (`absolutize`), reporting
3
+ // what moved to onLayout and to the layout diff.
4
+
5
+ import { Yoga } from '../yoga.js';
6
+ import { callHandler } from '../errors.js';
7
+ import { DAMAGE_SLOP, layoutDiff } from './damage.js';
8
+ import { insetRect } from './rects.js';
9
+ import { DEV } from './util.js';
10
+
11
+ export const MEASURE_MODES = [];
12
+ MEASURE_MODES[Yoga.MEASURE_MODE_UNDEFINED] = 'unconstrained';
13
+ MEASURE_MODES[Yoga.MEASURE_MODE_EXACTLY] = 'exactly';
14
+ MEASURE_MODES[Yoga.MEASURE_MODE_AT_MOST] = 'at-most';
15
+
16
+ /**
17
+ * The pixels on offer on one axis, as a number an element can do arithmetic
18
+ * with. Yoga says "no bound" with a null, so `Math.min(preferred, width)`
19
+ * would answer 0 to the one question where the honest answer is `preferred`;
20
+ * `Infinity` is what "no bound" means in that expression, and it makes the
21
+ * mode something an element consults only when it has a reason to.
22
+ */
23
+ export function measureOffer(value, mode) {
24
+ return mode === Yoga.MEASURE_MODE_UNDEFINED || !Number.isFinite(value)
25
+ ? Infinity
26
+ : value;
27
+ }
28
+
29
+ /** What a measure function answered, for the error that rejects it. */
30
+ export function describeSize(size) {
31
+ if (size === null || typeof size !== 'object') return String(size);
32
+ return `{ width: ${size.width}, height: ${size.height} }`;
33
+ }
34
+
35
+ /**
36
+ * Fit a natural size into what layout offered — the shape `<image>`, `<svg>`
37
+ * and any other element whose content has a size of its own and an aspect
38
+ * ratio to keep.
39
+ *
40
+ * **Which axes the style fixed is something layout already knows**, and says
41
+ * in the measure modes: `'exactly'` on an axis means the style made it
42
+ * definite. Working it out a second time by reading style back would be
43
+ * duplication; working it out by reading *props* was issue #118 — `width` is
44
+ * a style name, so `<image width={40}>` throws in development and only ever
45
+ * reached that branch in production.
46
+ *
47
+ * - Both fixed: layout skips the measure entirely, so there is no
48
+ * fixed-size case to write here.
49
+ * - Height fixed alone: scale the width with it, the way an `<img>` with
50
+ * only a height set does, rather than stretching to the container.
51
+ * - Otherwise: natural size, shrunk to the width on offer, height following
52
+ * the aspect ratio.
53
+ *
54
+ * @param {{width: number, height: number}} natural the content's own size
55
+ * @param {MeasureConstraints} constraints the argument of `measureContent`
56
+ */
57
+ export function intrinsicSize(
58
+ natural,
59
+ { width, height, widthMode, heightMode },
60
+ ) {
61
+ const { width: natW, height: natH } = natural;
62
+ if (heightMode === 'exactly' && widthMode !== 'exactly' && natH > 0) {
63
+ return { width: (height * natW) / natH, height };
64
+ }
65
+ const w = width < natW ? width : natW;
66
+ return { width: w, height: natW > 0 ? (w * natH) / natW : natH };
67
+ }
68
+
69
+ /**
70
+ * Where layout put `node` inside its parent's border box — yoga's offset,
71
+ * plus, for a child a layout algorithm placed, the slot it was placed in
72
+ * (its own yoga tree is a root, and a root's offset is only its margin).
73
+ * The one sum `absolutize`, `onLayout` and `scrollIntoView` all make.
74
+ */
75
+ export function offsetInParent(node) {
76
+ const slot =
77
+ node.parent !== null && node.parent._host !== null ? node._hostSlot : null;
78
+ const yoga = node.yoga;
79
+ return {
80
+ x: yoga.getComputedLeft() + (slot === null ? 0 : slot.x),
81
+ y: yoga.getComputedTop() + (slot === null ? 0 : slot.y),
82
+ };
83
+ }
84
+
85
+ /** Layout, installed onto `Node.prototype` by node.js. */
86
+ export class NodeLayout {
87
+ /**
88
+ * Give this node's box a measure function, keeping a reference that can be
89
+ * asked again later.
90
+ *
91
+ * A leaf's content is recorded nowhere but in its measure function, and
92
+ * the size yoga keeps for it is not always what that function said:
93
+ * `align-items` defaults to `stretch`, so in a pass run with no room on
94
+ * offer — which is how a content floor is measured, see `contentSpan` —
95
+ * the cross size a leaf ends up at is the container's, not its own. A
96
+ * container in that position is recovered by looking inside it. A leaf
97
+ * has nothing inside, so it is asked again instead.
98
+ */
99
+ _setMeasureFunc(measure) {
100
+ this._measureFn = measure;
101
+ this.yoga.setMeasureFunc(measure);
102
+ }
103
+
104
+ /**
105
+ * The height this leaf takes at `width` with nothing bounding its height
106
+ * — the answer yoga gets from the measuring pass that settles the height
107
+ * floors, asked directly. `probeHeightFloors` asks it twice, for the width
108
+ * a leaf was measured at and the one it has now, to find out whether a
109
+ * relayout that moved the leaf changed what it needs; a paragraph answers
110
+ * from its layout cache, and the elements whose height is not a function
111
+ * of their width at all answer at once.
112
+ */
113
+ _heightForWidth(width) {
114
+ if (this._host !== null) {
115
+ return this._measureHost(
116
+ width,
117
+ this._floorMeasureMode === 'exactly'
118
+ ? Yoga.MEASURE_MODE_EXACTLY
119
+ : Yoga.MEASURE_MODE_AT_MOST,
120
+ Number.NaN,
121
+ Yoga.MEASURE_MODE_UNDEFINED,
122
+ ).height;
123
+ }
124
+ return this.measureContent({
125
+ width,
126
+ height: Infinity,
127
+ widthMode: this._floorMeasureMode ?? 'at-most',
128
+ heightMode: 'unconstrained',
129
+ })?.height;
130
+ }
131
+
132
+ /**
133
+ * Hand `measureContent` to layout, translated: the modes arrive as words,
134
+ * an axis with no bound arrives as `Infinity` rather than as yoga's null,
135
+ * and what comes back is checked before it can turn a whole tree into
136
+ * NaNs. Called by the constructor, so an element only writes the method.
137
+ */
138
+ _useMeasureContent() {
139
+ this._setMeasureFunc((width, widthMode, height, heightMode) => {
140
+ if (heightMode === Yoga.MEASURE_MODE_UNDEFINED) {
141
+ this._floorMeasureMode = MEASURE_MODES[widthMode];
142
+ }
143
+ const size = this.measureContent({
144
+ width: measureOffer(width, widthMode),
145
+ height: measureOffer(height, heightMode),
146
+ widthMode: MEASURE_MODES[widthMode],
147
+ heightMode: MEASURE_MODES[heightMode],
148
+ });
149
+ if (!Number.isFinite(size?.width) || !Number.isFinite(size?.height)) {
150
+ // Left to itself this is a destructuring TypeError from inside
151
+ // yoga's wrapper, or — worse, because it does not throw at all — a
152
+ // NaN that spreads through every ancestor's rect.
153
+ throw new Error(
154
+ `react-x11: <${this.kind}>.measureContent() must return ` +
155
+ '{ width, height } as finite numbers; it returned ' +
156
+ `${describeSize(size)}. Return { width: 0, height: 0 } for ` +
157
+ 'content that has not arrived yet.',
158
+ );
159
+ }
160
+ return size;
161
+ });
162
+ }
163
+
164
+ /**
165
+ * The inputs to `measureContent` changed — a prop it reads, data that
166
+ * loaded — so the next layout has to ask again instead of reusing the
167
+ * answer it cached.
168
+ *
169
+ * `reason` joins the closed set the diagnostics print (docs/debugging.md);
170
+ * the default says the measurement itself moved.
171
+ */
172
+ invalidateMeasure(reason = 'measure') {
173
+ // Nothing to re-ask, and both halves matter: layout aborts the process
174
+ // on a node that never had a measure function, and a destroyed node's
175
+ // box has already been freed under it.
176
+ if (this.destroyed || !this._measureFn) {
177
+ // Said once, in development: an element that asks for a re-measure and
178
+ // silently gets none looks broken rather than degraded, and there is
179
+ // nothing in the frame to pull on.
180
+ if (DEV && !this.destroyed && !this._measureNagged) {
181
+ this._measureNagged = true;
182
+ console.warn(
183
+ `react-x11: <${this.kind}>.invalidateMeasure() has nothing to ` +
184
+ 're-measure — this element implements no measureContent(). ' +
185
+ 'Note it has to be a method on the class: assigning it in the ' +
186
+ 'constructor is too late, since the base Node constructor is ' +
187
+ 'what wires it to layout.',
188
+ );
189
+ }
190
+ return;
191
+ }
192
+ this.yoga.markDirty();
193
+ this._invalidateLayout(reason);
194
+ }
195
+
196
+ absolutize(originX, originY) {
197
+ // before the yoga check, so a span — placed by its paragraph, no box of
198
+ // its own — counts as on screen too
199
+ this._placed = true;
200
+ if (!this.yoga) return;
201
+ this._assignAbs(
202
+ originX + this.yoga.getComputedLeft(),
203
+ originY + this.yoga.getComputedTop(),
204
+ this.yoga.getComputedWidth(),
205
+ this.yoga.getComputedHeight(),
206
+ );
207
+ if (this.props.onLayout) this._reportLayout();
208
+ if (this._host !== null) {
209
+ this._absolutizeHostChildren();
210
+ return;
211
+ }
212
+ for (const child of this.children) {
213
+ if (!child.isWindow) child.absolutize(this.abs.x, this.abs.y);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * `onLayout`: the rect a layout pass gave this node, reported when it
219
+ * changed — React Native's contract, and the seam for a decision that is
220
+ * not a style (docs/react-features.md): how many columns to build, which
221
+ * component to render. Where the decision *is* a style, a container
222
+ * query answers it in the same frame instead (docs/styling.md).
223
+ *
224
+ * `x`/`y` are the position **within the parent as laid out** — yoga's
225
+ * answer, which a scroll does not move — rather than the window
226
+ * coordinates `abs` holds: a list scrolling under the pointer must not
227
+ * re-render every row on every notch. Logical pixels, this node's own,
228
+ * the same division `measure()` makes.
229
+ *
230
+ * Deferred, like `onViewport`: this runs inside the layout pass, and a
231
+ * `setState` from the handler would re-enter it. One report per frame,
232
+ * because `absolutize` runs once, after the container blocks have
233
+ * settled — a card whose block changed its height reports the height it
234
+ * ended the frame at, not the one it had between passes.
235
+ */
236
+ _reportLayout() {
237
+ const s = this.scale;
238
+ const offset = offsetInParent(this);
239
+ const next = {
240
+ x: offset.x / s,
241
+ y: offset.y / s,
242
+ width: this.abs.width / s,
243
+ height: this.abs.height / s,
244
+ };
245
+ const last = this._lastLayout;
246
+ if (
247
+ last &&
248
+ last.x === next.x &&
249
+ last.y === next.y &&
250
+ last.width === next.width &&
251
+ last.height === next.height
252
+ ) {
253
+ return;
254
+ }
255
+ this._lastLayout = next;
256
+ setImmediate(() => {
257
+ // the handler as it is *now*: React may have re-rendered in between
258
+ const notify = this.props.onLayout;
259
+ if (this.destroyed || !notify) return;
260
+ callHandler(this, 'onLayout', notify, next);
261
+ });
262
+ }
263
+
264
+ /**
265
+ * absolutize's write to `abs`, funneled through one place so a bounded
266
+ * frame's layout diff sees every node the pass actually moved or resized.
267
+ * The old and new rects are claimed separately (not their union box —
268
+ * a node crossing the window would drag everything between them along),
269
+ * each grown by this node's own paint reach. A rect that was or became
270
+ * zero-area claims nothing: there were, or will be, no pixels there.
271
+ */
272
+ _assignAbs(x, y, width, height) {
273
+ const old = this.abs;
274
+ if (
275
+ old.x === x &&
276
+ old.y === y &&
277
+ old.width === width &&
278
+ old.height === height
279
+ ) {
280
+ return;
281
+ }
282
+ this.abs = { x, y, width, height };
283
+ // moving or resizing changes where this subtree can be hit, and the
284
+ // cached unions all the way up with it
285
+ this._clearHitBounds();
286
+ if (layoutDiff.sink) {
287
+ const grow = this._outlineExtent() + DAMAGE_SLOP;
288
+ const shift = layoutDiff.shift;
289
+ const had = old.width > 0 && old.height > 0;
290
+ if (shift) {
291
+ // Riding a blit (issue #398): the rect this node *would* have had if
292
+ // nothing but the scroll had happened. Landing there is the blit's
293
+ // own translation and claims nothing — claiming it would repaint the
294
+ // band the blit exists to keep. Landing anywhere else is a real move,
295
+ // and both ends of it are claimed in post-blit coordinates, which is
296
+ // where the frame will paint them.
297
+ const was = {
298
+ x: old.x + shift.x,
299
+ y: old.y + shift.y,
300
+ width: old.width,
301
+ height: old.height,
302
+ };
303
+ if (
304
+ had &&
305
+ was.x === x &&
306
+ was.y === y &&
307
+ old.width === width &&
308
+ old.height === height
309
+ ) {
310
+ return;
311
+ }
312
+ if (had) layoutDiff.sink(insetRect(was, -grow));
313
+ if (width > 0 && height > 0) {
314
+ layoutDiff.sink(insetRect(this.abs, -grow));
315
+ }
316
+ return;
317
+ }
318
+ if (had) {
319
+ layoutDiff.sink(insetRect(old, -grow));
320
+ }
321
+ if (width > 0 && height > 0) {
322
+ layoutDiff.sink(insetRect(this.abs, -grow));
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Move an already-laid-out subtree by a constant, without asking yoga
329
+ * anything — the scroll fast path's walk (issue #405).
330
+ *
331
+ * A pure-scroll frame changes nothing about the arrangement inside a
332
+ * viewport: every descendant sits exactly where the last pass put it,
333
+ * shifted by the scroll delta. `absolutize` would re-derive each rect
334
+ * through four wasm-boundary getters to learn what one addition already
335
+ * says, so the scroller calls this instead — only after proving nothing
336
+ * inside was laid out this pass (see `_absolutizeChildren`).
337
+ *
338
+ * `abs` is adjusted in place rather than replaced: its identity is
339
+ * already long-lived (`_assignAbs` keeps the object whenever a rect is
340
+ * unchanged), and everything that records a rect for later copies it.
341
+ * The cached hit bounds ride along instead of being dropped — a uniform
342
+ * translation is the one change a cached union survives — which keeps a
343
+ * wheel flick from rebuilding the pane's whole hit-bounds tree per notch.
344
+ *
345
+ * No layout diff runs here, and none is owed: under a blit ledger the
346
+ * shifted diff's claims are the *deviations* from exactly this
347
+ * translation, and a subtree nothing laid out again has none.
348
+ */
349
+ _shiftAbs(dx, dy) {
350
+ if (!this.yoga) return;
351
+ const abs = this.abs;
352
+ abs.x += dx;
353
+ abs.y += dy;
354
+ // the paint reach rides along the same way — unless it *is* `abs`,
355
+ // which just moved
356
+ const p = this._paintBoundsCache;
357
+ if (p && p !== abs) {
358
+ p.x += dx;
359
+ p.y += dy;
360
+ }
361
+ const b = this._hitBoundsCache;
362
+ if (b) {
363
+ b.left += dx;
364
+ b.right += dx;
365
+ b.top += dy;
366
+ b.bottom += dy;
367
+ }
368
+ this._shiftChildren(dx, dy);
369
+ }
370
+
371
+ /** Split from `_shiftAbs` so a scroller can reroute its children through
372
+ * its own offset bookkeeping — the box moves rigidly, but the children's
373
+ * origin also carries scroll offsets that may have changed again this
374
+ * same frame (`Scrollable._shiftChildren`). */
375
+ _shiftChildren(dx, dy) {
376
+ for (const child of this.children) {
377
+ if (!child.isWindow) child._shiftAbs(dx, dy);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * A layout-affecting change at this node may change how far the content
383
+ * of an enclosing scroll pane reaches through a route yoga never
384
+ * witnesses — an element that paints its own content growing its extent
385
+ * announces it with `invalidate(true, this, 'scroll')`
386
+ * (docs/extending.md), and no yoga node is dirtied by that. Mark every
387
+ * scroller whose measurement can see this node, so the next pass asks
388
+ * `measureScrollContent` again instead of reusing the cached reach
389
+ * (issue #405). The walk stops where the measurement does: at the first
390
+ * ancestor that clips its children, whose overflow is its own business.
391
+ */
392
+ _markScrollMeasureDirty() {
393
+ for (let n = this; n; n = n.parent) {
394
+ // only a Scrollable carries the flag; a stale `true` on a box that is
395
+ // not currently a scroller costs nothing and re-measures correctly if
396
+ // its style later makes it one
397
+ if (n._scrollMeasureDirty === false) n._scrollMeasureDirty = true;
398
+ if (n !== this && n.clipsChildren()) return;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * The rectangle this node's **content** goes in — `abs` inset by the
404
+ * border and the padding, in the owning window's coordinates. Every text
405
+ * element in core paints inside it, and so should anything a registered
406
+ * element draws that the padding is meant to hold off.
407
+ *
408
+ * Public (docs/extending.md) because the arithmetic is not reproducible
409
+ * from `this.style`: the insets come off the yoga node, which is where
410
+ * percentages, the per-side overrides and the border widths have already
411
+ * been resolved against this frame's size. An element deriving them from
412
+ * the style bag instead re-implements a resolution order it cannot see,
413
+ * and silently disagrees with `<text>` the day the vocabulary grows
414
+ * another edge — the per-side border widths (#262) were the last one.
415
+ */
416
+ contentBox() {
417
+ // A node with no yoga node (`{ yoga: false }`) has no resolved insets,
418
+ // so its box is its content box.
419
+ if (!this.yoga) return { ...this.abs };
420
+ const padL =
421
+ this.yoga.getComputedPadding(Yoga.EDGE_LEFT) +
422
+ this.yoga.getComputedBorder(Yoga.EDGE_LEFT);
423
+ const padT =
424
+ this.yoga.getComputedPadding(Yoga.EDGE_TOP) +
425
+ this.yoga.getComputedBorder(Yoga.EDGE_TOP);
426
+ const padR =
427
+ this.yoga.getComputedPadding(Yoga.EDGE_RIGHT) +
428
+ this.yoga.getComputedBorder(Yoga.EDGE_RIGHT);
429
+ const padB =
430
+ this.yoga.getComputedPadding(Yoga.EDGE_BOTTOM) +
431
+ this.yoga.getComputedBorder(Yoga.EDGE_BOTTOM);
432
+ return {
433
+ x: this.abs.x + padL,
434
+ y: this.abs.y + padT,
435
+ width: Math.max(0, this.abs.width - padL - padR),
436
+ height: Math.max(0, this.abs.height - padT - padB),
437
+ };
438
+ }
439
+ }