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,185 @@
1
+ // Hit testing, and the geometry a node reports about itself: containsPoint
2
+ // with and without slop, hitTest down the paint order, getClientRects, and
3
+ // the box-model measure DevTools reads.
4
+
5
+ import { resolveHitSlop } from '../styles.js';
6
+
7
+ /** Hit testing, installed onto `Node.prototype` by node.js. */
8
+ export class NodeHitTest {
9
+ containsPoint(x, y) {
10
+ return (
11
+ x >= this.abs.x &&
12
+ y >= this.abs.y &&
13
+ x < this.abs.x + this.abs.width &&
14
+ y < this.abs.y + this.abs.height
15
+ );
16
+ }
17
+
18
+ /**
19
+ * The same test grown by `hitSlop`. Deliberately separate from
20
+ * `containsPoint`: slop belongs to *this* node's target and nothing else,
21
+ * so it must not widen the rect that clips this node's children, must not
22
+ * reach `paintBounds`, and must never touch yoga. A 16px slider with 4px
23
+ * of slop top and bottom is a 24px target that still draws 16px tall,
24
+ * which is the whole point — WCAG 2.2 SC 2.5.8 without a redesign.
25
+ *
26
+ * The slop can overlap a sibling's box. Hit testing is front-to-back over
27
+ * paint order, so the sibling on top keeps its own pixels either way.
28
+ */
29
+ containsPointWithSlop(x, y) {
30
+ if (this.containsPoint(x, y)) return true;
31
+ const slop = resolveHitSlop(this.style.hitSlop);
32
+ if (!slop) return false;
33
+ return (
34
+ x >= this.abs.x - slop.left &&
35
+ y >= this.abs.y - slop.top &&
36
+ x < this.abs.x + this.abs.width + slop.right &&
37
+ y < this.abs.y + this.abs.height + slop.bottom
38
+ );
39
+ }
40
+
41
+ /** DOM-ish rect accessor. React DevTools' Highlighter requires host
42
+ * instances to expose getClientRects() with a non-empty rect before it
43
+ * emits showNativeHighlight — without this, hovering the tree silently
44
+ * no-ops. Anything with getClientRects is also measured at mount via
45
+ * `instance.ownerDocument.documentElement` (see ownerDocument below). */
46
+ getClientRects() {
47
+ const r = this.abs;
48
+ if (!(r.width > 0 || r.height > 0)) return [];
49
+ // `abs` is device pixels; this is public API, which is logical — the
50
+ // same conversion events make on the way out (src/scale.js)
51
+ const s = this.scale;
52
+ return [
53
+ {
54
+ x: r.x / s,
55
+ y: r.y / s,
56
+ left: r.x / s,
57
+ top: r.y / s,
58
+ width: r.width / s,
59
+ height: r.height / s,
60
+ right: (r.x + r.width) / s,
61
+ bottom: (r.y + r.height) / s,
62
+ },
63
+ ];
64
+ }
65
+
66
+ /**
67
+ * React Native's measure contract, which is what DevTools' style editor
68
+ * calls to draw the box model beside the style it is editing:
69
+ * `(x, y, width, height, left, top)` — position within the parent, size,
70
+ * then position within the window. A node with no laid-out rect calls
71
+ * back with nothing, the "unmeasurable" answer the editor checks for.
72
+ */
73
+ measure(callback) {
74
+ if (typeof callback !== 'function') return;
75
+ const r = this.abs;
76
+ if (!(r.width > 0 || r.height > 0)) {
77
+ callback();
78
+ return;
79
+ }
80
+ const s = this.scale;
81
+ const parent = this.parent?.abs;
82
+ callback(
83
+ (parent ? r.x - parent.x : r.x) / s,
84
+ (parent ? r.y - parent.y : r.y) / s,
85
+ r.width / s,
86
+ r.height / s,
87
+ r.x / s,
88
+ r.y / s,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * The rect a hit anywhere in this subtree must fall inside: this node's
94
+ * rect grown by its own hitSlop, unioned with every child's reach —
95
+ * except under a clipping node, whose children can only be hit while the
96
+ * point is inside its rect (hitTest never descends from outside one).
97
+ *
98
+ * A conservative superset, and only ever that: hidden and
99
+ * pointerEvents-none subtrees are counted anyway, and nothing shrinks a
100
+ * cached bound before the next invalidation. A bound too big costs a
101
+ * walk; one too small would drop real input. Every change that can
102
+ * *grow* the true reach funnels through `_clearHitBounds`: `_assignAbs`
103
+ * for whatever layout moves (mounts, reveals and scrolls all change
104
+ * `abs`), `_retarget` for `hitSlop` and `overflow`, `_childListChanged`
105
+ * for a subtree attached with its rect already laid out, and `flush`
106
+ * for the root's own rect, which is written without `_assignAbs`.
107
+ */
108
+ _hitBounds() {
109
+ let b = this._hitBoundsCache;
110
+ if (b) return b;
111
+ const abs = this.abs;
112
+ const slop = resolveHitSlop(this.style.hitSlop);
113
+ let left = abs.x;
114
+ let top = abs.y;
115
+ let right = abs.x + abs.width;
116
+ let bottom = abs.y + abs.height;
117
+ if (slop) {
118
+ left -= slop.left;
119
+ top -= slop.top;
120
+ right += slop.right;
121
+ bottom += slop.bottom;
122
+ }
123
+ if (!this.clipsChildren()) {
124
+ for (const child of this.children) {
125
+ if (child.isWindow || !child.yoga) continue;
126
+ const cb = child._hitBounds();
127
+ if (cb.left < left) left = cb.left;
128
+ if (cb.top < top) top = cb.top;
129
+ if (cb.right > right) right = cb.right;
130
+ if (cb.bottom > bottom) bottom = cb.bottom;
131
+ }
132
+ }
133
+ b = { left, top, right, bottom };
134
+ this._hitBoundsCache = b;
135
+ return b;
136
+ }
137
+
138
+ /**
139
+ * This node's reach changed: drop the cached bounds here and up the
140
+ * chain of ancestors whose unions embed them. The walk stops at a
141
+ * clipping ancestor — its reach is its own rect, so nothing below it
142
+ * changes what it or anything above it reports — or at one already
143
+ * invalid, whose own clear walked the rest of the way up.
144
+ */
145
+ _clearHitBounds() {
146
+ // what moves a hit reach moves the paint reach — first, because the
147
+ // walk below returns from wherever it finds an ancestor already clear
148
+ this._clearPaintBounds();
149
+ this._hitBoundsCache = null;
150
+ for (let n = this.parent; n; n = n.parent) {
151
+ if (n.clipsChildren() || n._hitBoundsCache === null) return;
152
+ n._hitBoundsCache = null;
153
+ }
154
+ }
155
+
156
+ /** Front-to-back hit test. Returns the deepest hit node or null. */
157
+ hitTest(x, y) {
158
+ if (
159
+ this.hidden ||
160
+ this.style.display === 'none' ||
161
+ this.style.pointerEvents === 'none'
162
+ ) {
163
+ return null;
164
+ }
165
+ // nothing in this subtree reaches the point: skip it whole, children
166
+ // and all — this is what keeps a motion event from walking every node
167
+ // in the window (issue #188)
168
+ const b = this._hitBounds();
169
+ if (x < b.left || y < b.top || x >= b.right || y >= b.bottom) {
170
+ return null;
171
+ }
172
+ const inside = this.containsPoint(x, y);
173
+ // the children are culled on the strict rect — slop grows this node's
174
+ // target, not the region its clip lets through
175
+ if (!inside && this.clipsChildren()) {
176
+ return this.containsPointWithSlop(x, y) ? this : null;
177
+ }
178
+ const order = this.paintOrder();
179
+ for (let i = order.length - 1; i >= 0; i--) {
180
+ const hit = order[i].hitTest(x, y);
181
+ if (hit) return hit;
182
+ }
183
+ return inside || this.containsPointWithSlop(x, y) ? this : null;
184
+ }
185
+ }
@@ -0,0 +1,266 @@
1
+ // <image>: an image source, loaded or decoded, sized from its intrinsic size
2
+ // and drawn into the node's box.
3
+
4
+ import {
5
+ DrawableSource,
6
+ PictureSource,
7
+ acquireImageSource,
8
+ decodeImageSource,
9
+ imageSourceChanged,
10
+ isDirectImageSource,
11
+ isPathImageSource,
12
+ isRawImageSource,
13
+ releaseImageSource,
14
+ toLoadablePath,
15
+ validateImageProps,
16
+ } from '../imagesource.js';
17
+ // Namespace import for `Surface`, the same shape and the same reason as
18
+ // `paintcache.js`: a named import of something an older ntk does not export
19
+ // is a *load-time* SyntaxError, which would take the renderer down rather
20
+ // than the one feature that needs it.
21
+ import * as ntk from 'ntk';
22
+ import { intrinsicSize } from './layout.js';
23
+ import { Node } from './node.js';
24
+ import { DEV } from './util.js';
25
+
26
+ /** The props `ImageNode.applyProps` diffs by value itself, kept out of the
27
+ * base class's identity walk — a buffer rebuilt under an unchanged
28
+ * `cacheKey` and a `{ id, … }` descriptor rebuilt with the same numbers are
29
+ * both "nothing changed". */
30
+ const IMAGE_SOURCE_PROPS = new Set(['src', 'picture', 'drawable', 'cacheKey']);
31
+
32
+ export class ImageNode extends Node {
33
+ constructor(props, app) {
34
+ super('image', props, app);
35
+ validateImageProps(props);
36
+ this.image = null;
37
+ this._loadToken = 0;
38
+ /** hold on the `cacheKey` cache entry, when one is held */
39
+ this._hold = null;
40
+ /** an Image decoded for this node alone (no cacheKey), freed on release */
41
+ this._ownedImage = null;
42
+ /** PictureSource/DrawableSource, when the source is server-side */
43
+ this._serverSource = null;
44
+ // Resolution waits for the first layout/paint: the constructor runs in
45
+ // the render phase, which React may discard, and resolving here would
46
+ // start file reads and take cache holds nothing would ever release.
47
+ this._sourceDirty = true;
48
+ }
49
+
50
+ get selfDamagedProps() {
51
+ return IMAGE_SOURCE_PROPS;
52
+ }
53
+
54
+ paintChanged(next, prev) {
55
+ return imageSourceChanged(next, prev) || super.paintChanged(next, prev);
56
+ }
57
+
58
+ /** The source's size, kept to its aspect ratio. Read per measure rather
59
+ * than once, because a decode can arrive late.
60
+ *
61
+ * Source pixels are *logical* pixels, the browser's rule: a 100px-wide
62
+ * PNG occupies 100 logical px at any display scale (upscaled onto the
63
+ * device grid at 2x, the way an `<img>` without `srcset` is), rather
64
+ * than shrinking to half its neighbours' size. The constraints are
65
+ * already device, so only the natural size converts (src/scale.js). */
66
+ measureContent(constraints) {
67
+ this._ensureSource();
68
+ const s = this.scale;
69
+ return intrinsicSize(
70
+ {
71
+ width: (this.image?.width ?? 0) * s,
72
+ height: (this.image?.height ?? 0) * s,
73
+ },
74
+ constraints,
75
+ );
76
+ }
77
+
78
+ /** First layout or paint after a mount or a source change — both run
79
+ * after commit, so an instance a concurrent render threw away never
80
+ * resolves anything. */
81
+ _ensureSource() {
82
+ if (!this._sourceDirty || this.destroyed) return;
83
+ this._sourceDirty = false;
84
+ this._resolveSource();
85
+ }
86
+
87
+ _resolveSource() {
88
+ const { src, picture, drawable, cacheKey } = this.props;
89
+ if (picture != null || drawable != null) {
90
+ this._serverSource =
91
+ picture != null
92
+ ? new PictureSource(this.app, picture)
93
+ : new DrawableSource(this.app, drawable);
94
+ this.image = this._serverSource;
95
+ return;
96
+ }
97
+ if (src == null) return;
98
+ if (isDirectImageSource(src)) {
99
+ // the caller's object — its upload cache is the dedupe, and it is
100
+ // never destroyed here
101
+ this.image = src;
102
+ return;
103
+ }
104
+ if (cacheKey != null) {
105
+ const entry = acquireImageSource(this.app, cacheKey, () =>
106
+ this._loadEntry(src),
107
+ );
108
+ this._hold = entry;
109
+ if (entry.image) {
110
+ this.image = entry.image;
111
+ if (DEV) this._devCheckCacheKey(src, entry.image, cacheKey);
112
+ } else {
113
+ entry.promise?.then((image) => {
114
+ if (image && this._hold === entry && !this.destroyed) {
115
+ this._setImage(image);
116
+ }
117
+ });
118
+ }
119
+ return;
120
+ }
121
+ if (isPathImageSource(src)) {
122
+ this._loadFile(src);
123
+ return;
124
+ }
125
+ const image = this._decode(src);
126
+ if (image) {
127
+ this._ownedImage = image;
128
+ this.image = image;
129
+ }
130
+ }
131
+
132
+ /** `{ image }` or `{ promise }` for the cache. Failures resolve to null
133
+ * and are reported once, here — not per node holding the key. */
134
+ _loadEntry(src) {
135
+ if (isPathImageSource(src)) {
136
+ return {
137
+ promise: ntk.loadImage(toLoadablePath(src)).then(
138
+ (image) => image,
139
+ (err) => {
140
+ console.error(
141
+ `react-x11: failed to load image ${src}:`,
142
+ err.message,
143
+ );
144
+ return null;
145
+ },
146
+ ),
147
+ };
148
+ }
149
+ return { image: this._decode(src) };
150
+ }
151
+
152
+ _decode(src) {
153
+ try {
154
+ return decodeImageSource(src);
155
+ } catch (err) {
156
+ // corrupt or unrecognized bytes are a content failure, not a
157
+ // programming error: log and show nothing, like a missing file
158
+ console.error(
159
+ 'react-x11: <image src> bytes did not decode:',
160
+ err.message,
161
+ );
162
+ return null;
163
+ }
164
+ }
165
+
166
+ async _loadFile(src) {
167
+ const token = ++this._loadToken;
168
+ try {
169
+ const image = await ntk.loadImage(toLoadablePath(src));
170
+ if (token !== this._loadToken || this.destroyed) return;
171
+ this._ownedImage = image;
172
+ this._setImage(image);
173
+ } catch (err) {
174
+ if (token === this._loadToken && !this.destroyed) {
175
+ console.error(`react-x11: failed to load image ${src}:`, err.message);
176
+ }
177
+ }
178
+ }
179
+
180
+ /** Adopt pixels that arrived after the frame that asked for them. A size
181
+ * change reflows; same-size new pixels claim only this node's box. */
182
+ _setImage(image) {
183
+ const prev = this.image;
184
+ this.image = image;
185
+ if (
186
+ (prev?.width ?? 0) !== (image?.width ?? 0) ||
187
+ (prev?.height ?? 0) !== (image?.height ?? 0)
188
+ ) {
189
+ this.invalidateMeasure('content');
190
+ } else {
191
+ this.root?.invalidate(false, this, 'content');
192
+ }
193
+ }
194
+
195
+ /** Two different pictures sharing one cacheKey is the one mistake this
196
+ * design can make show stale pixels; the raw form carries enough to catch
197
+ * the common case cheaply. */
198
+ _devCheckCacheKey(src, image, key) {
199
+ if (!isRawImageSource(src)) return;
200
+ if (src.width === image.width && src.height === image.height) return;
201
+ console.error(
202
+ `react-x11: <image cacheKey=${JSON.stringify(key)}> is ` +
203
+ `${image.width}x${image.height} in the cache, but this src says ` +
204
+ `${src.width}x${src.height}. Two different pictures are sharing one ` +
205
+ 'cacheKey — the key must name the content, so include whatever ' +
206
+ 'distinguishes them.',
207
+ );
208
+ }
209
+
210
+ _releaseSource() {
211
+ this._loadToken++; // orphan any in-flight file read
212
+ if (this._hold) {
213
+ releaseImageSource(this.app, this._hold);
214
+ this._hold = null;
215
+ }
216
+ if (this._ownedImage) {
217
+ // frees the per-app upload; the caller's own Images are never here
218
+ this._ownedImage.destroy();
219
+ this._ownedImage = null;
220
+ }
221
+ if (this._serverSource) {
222
+ this._serverSource.destroy?.();
223
+ this._serverSource = null;
224
+ }
225
+ this.image = null;
226
+ }
227
+
228
+ applyProps(newProps, oldProps) {
229
+ const before = oldProps ?? this.props;
230
+ const sourceChanged = imageSourceChanged(newProps, before);
231
+ // before super touches anything, so a bad update leaves the node whole
232
+ if (sourceChanged) validateImageProps(newProps);
233
+ super.applyProps(newProps, oldProps);
234
+ if (!sourceChanged) return;
235
+ const prev = this.image;
236
+ this._releaseSource();
237
+ this._sourceDirty = false;
238
+ this._resolveSource();
239
+ // paintChanged already claimed this node's box through super; only a
240
+ // new intrinsic size needs more than that
241
+ if (
242
+ (prev?.width ?? 0) !== (this.image?.width ?? 0) ||
243
+ (prev?.height ?? 0) !== (this.image?.height ?? 0)
244
+ ) {
245
+ this.invalidateMeasure('content');
246
+ }
247
+ }
248
+
249
+ destroySubtree() {
250
+ this._releaseSource();
251
+ super.destroySubtree();
252
+ }
253
+
254
+ paintContent(ctx) {
255
+ this._ensureSource();
256
+ if (!this.image) return;
257
+ const content = this.contentBox();
258
+ ctx.drawImage(
259
+ this.image,
260
+ content.x,
261
+ content.y,
262
+ content.width,
263
+ content.height,
264
+ );
265
+ }
266
+ }
@@ -0,0 +1,75 @@
1
+ // Puts a concern's methods onto the class they belong to.
2
+ //
3
+ // The node classes are split by concern rather than by class: the scroll
4
+ // blit's methods live in scrollblit.js whether they are Node's or
5
+ // WindowNode's, the paint walk's in paint.js, and so on. Each concern writes
6
+ // its methods as the body of a class that is never instantiated — a part —
7
+ // and the class's own file installs its parts right after declaring it:
8
+ //
9
+ // installMethods(Node, NodeStyling, NodeCascade, …);
10
+ //
11
+ // What the class ends up with is what one class body would have given it: the
12
+ // same function objects, as non-enumerable properties of the same prototype.
13
+ // Anything that compares against a prototype (`node.paint !==
14
+ // Node.prototype.paint`) or overrides a method in a subclass sees no
15
+ // difference.
16
+ //
17
+ // Two guards keep the arrangement safe to edit. A name defined twice — in the
18
+ // class and a part, or in two parts — throws while the module loads, instead
19
+ // of whichever ran last silently winning. And each part's prototype is
20
+ // re-parented onto the class's parent, so `super` in a moved method means
21
+ // what it meant in the class body.
22
+
23
+ /** part -> the class it was installed onto */
24
+ const installed = new WeakMap();
25
+
26
+ /** prototype -> (key -> the part that defined it) */
27
+ const origins = new WeakMap();
28
+
29
+ /**
30
+ * Install each part's methods and accessors onto `target.prototype`.
31
+ *
32
+ * @param {Function} target the class
33
+ * @param {...Function} parts classes whose bodies hold the rest of its methods
34
+ */
35
+ export function installMethods(target, ...parts) {
36
+ const proto = target.prototype;
37
+ const parent = Object.getPrototypeOf(proto);
38
+ if (!origins.has(proto)) origins.set(proto, new Map());
39
+ const defined = origins.get(proto);
40
+ for (const part of parts) {
41
+ if (installed.has(part)) {
42
+ throw new Error(
43
+ `react-x11: ${part.name} is already installed onto ` +
44
+ `${installed.get(part).name}; a part belongs to exactly one class.`,
45
+ );
46
+ }
47
+ const keys = Reflect.ownKeys(part.prototype).filter(
48
+ (key) => key !== 'constructor',
49
+ );
50
+ for (const key of keys) {
51
+ if (Object.hasOwn(proto, key)) {
52
+ const first = defined.get(key) ?? `the ${target.name} class body`;
53
+ throw new Error(
54
+ `react-x11: ${target.name}.${String(key)} is defined twice, in ` +
55
+ `${first} and in ${part.name}. A method lives in exactly one ` +
56
+ `file under src/nodes/ — rename one of them, or delete it.`,
57
+ );
58
+ }
59
+ }
60
+ Object.setPrototypeOf(part.prototype, parent);
61
+ for (const key of keys) {
62
+ Object.defineProperty(
63
+ proto,
64
+ key,
65
+ Object.getOwnPropertyDescriptor(part.prototype, key),
66
+ );
67
+ defined.set(key, part.name);
68
+ }
69
+ installed.set(part, target);
70
+ }
71
+ }
72
+
73
+ /** The class `part` was installed onto, or undefined: what the test that
74
+ * every part under src/nodes/ is installed somewhere reads. */
75
+ export const installedOnto = (part) => installed.get(part);