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
@@ -0,0 +1,868 @@
1
+ // The retained node tree's base class: one lightweight JS node per host
2
+ // element, one yoga node per drawn element, painted into the owning
3
+ // <window>'s single 2d context on ntk's frame clock. Only <window> owns a
4
+ // real X11 window (see NEXT_STEPS.md §4 for the rationale).
5
+ //
6
+ // This file is what every node is: construction, props, the child list and
7
+ // its yoga tree, focus and the default actions. The rest of Node lives with
8
+ // the concern each method serves — styling.js, cascade.js, layout.js,
9
+ // paint.js and the others beside this file — and installMethods() at the
10
+ // bottom puts it on the prototype. install.js explains the arrangement.
11
+
12
+ import {
13
+ applyLayoutStyle,
14
+ applyLayoutDefaults,
15
+ createLayoutNode,
16
+ paintPropsChanged,
17
+ isEventProp,
18
+ } from '../styles.js';
19
+ import { Yoga } from '../yoga.js';
20
+ import { isPlaced } from '../layouts.js';
21
+ import { synthesizeClick } from '../events.js';
22
+ import { hasDropProps } from '../dnd.js';
23
+ import {
24
+ hooks as a11yHooks,
25
+ devCheckA11yProps,
26
+ hasClickHandler,
27
+ } from '../a11y.js';
28
+ import { XK_RETURN, XK_KP_ENTER, XK_SPACE } from '../keysyms.js';
29
+ import { dropVisibleSelection, selectionSurfaceOf } from '../textselection.js';
30
+ import { NodeAnimation } from './animation.js';
31
+ import { NodeBoxPaint } from './boxpaint.js';
32
+ import { NodeCascade } from './cascade.js';
33
+ import { NO_DAMAGE } from './damage.js';
34
+ import { NodeHitTest } from './hittest.js';
35
+ import { installMethods } from './install.js';
36
+ import { NodeInvalidate } from './invalidate.js';
37
+ import { CUSTOM_SELF_DAMAGED } from './kinds.js';
38
+ import { NodeLayout } from './layout.js';
39
+ import { NodeLayoutHost } from './layouthost.js';
40
+ import { NodePaint } from './paint.js';
41
+ import { NodePosition } from './position.js';
42
+ import { NodeQueries } from './queries.js';
43
+ import { NodeScrollBlit } from './scrollblit.js';
44
+ import { NodeSelectable } from './selectable.js';
45
+ import { NodeStyling } from './styling.js';
46
+ import { DEV } from './util.js';
47
+
48
+ // DevTools' measureHostInstance dereferences instance.ownerDocument
49
+ // unconditionally once getClientRects exists; a null documentElement and
50
+ // defaultView give it zero scroll offsets and no crash.
51
+ export const DEVTOOLS_FAKE_DOCUMENT = {
52
+ documentElement: null,
53
+ defaultView: null,
54
+ };
55
+
56
+ // And the default for the other declaration: an element that has not said
57
+ // otherwise claims nothing for itself, so every prop it holds is core's to
58
+ // be conservative about.
59
+ const NO_SELF_DAMAGED = new Set();
60
+
61
+ /** The half of `Node._joinsYoga` that is about the child alone — a real X
62
+ * window (`<window>`, `<popup>`) or a node built without a box at all (a text
63
+ * chunk) sits outside whatever parent it lands in. This is what
64
+ * `_nonYogaKids` counts, so the count stays right for a parent that has no
65
+ * box of its own either. */
66
+ const outsideYoga = (child) => !child.yoga || child.isWindow;
67
+
68
+ export class Node {
69
+ get ownerDocument() {
70
+ return DEVTOOLS_FAKE_DOCUMENT;
71
+ }
72
+
73
+ constructor(kind, props, app, { yoga = true } = {}) {
74
+ this.kind = kind;
75
+ this.props = props;
76
+ this.app = app;
77
+ this.parent = null;
78
+ this.children = [];
79
+ // Where this node sits in `parent.children`, and how many of *this*
80
+ // node's children sit outside its yoga tree. Both are bookkeeping that
81
+ // turns the scans `insertBefore` used to do over the whole child list
82
+ // into constant work, which is what stops a commit that mounts a
83
+ // virtualized list's window from costing O(rows x pane) (issue #397).
84
+ // The index is a hint — `_indexOfChild` proves it before using it — and
85
+ // the count is exact, maintained by the three places `children` is
86
+ // spliced.
87
+ this._childIndex = -1;
88
+ this._nonYogaKids = 0;
89
+ // The pre-mutation bounds this frame already claimed for this node, so
90
+ // that a second mutation reuses the rect instead of walking the subtree
91
+ // again. Lives exactly as long as membership in `root._reflowed`.
92
+ this._reflowBefore = null;
93
+ // The automatic minimum size (#249), cached per node: what this node
94
+ // contributes to the box around it on each axis (`contentSpan`), the
95
+ // width the height was measured for (`probeHeightFloors`), and the
96
+ // floor yoga currently holds from us on each axis (`writeFloors`).
97
+ // `undefined` on an extent means it has to be measured again; `null`
98
+ // on a written floor means yoga's minimum may not be ours any more.
99
+ this._floorW = undefined;
100
+ this._floorH = undefined;
101
+ this._floorAtW = undefined;
102
+ this._floorMinW = undefined;
103
+ this._floorMinH = undefined;
104
+ // the width mode yoga last measured this leaf in with no height on
105
+ // offer — the question `_heightForWidth` repeats
106
+ this._floorMeasureMode = null;
107
+ this.root = null; // owning WindowNode once attached
108
+ this.hidden = false;
109
+ // Composited on a layer of its own above the window's bitmap, by a
110
+ // presenter that can (src/cocoa/promotion.js): the paint walk leaves a
111
+ // hole where it is, and the presenter draws it — the `<glarea>` idiom.
112
+ this._promoted = false;
113
+ this.destroyed = false;
114
+ // absolute rect within the owning window, filled by absolutize()
115
+ this.abs = { x: 0, y: 0, width: 0, height: 0 };
116
+ // node states that style blocks can react to, owned by EventManager
117
+ this.states = {
118
+ ':hover': false,
119
+ ':focus-within': false,
120
+ ':focus': false,
121
+ ':focus-visible': false,
122
+ ':active': false,
123
+ ':drag-over': false,
124
+ ':dragging': false,
125
+ };
126
+ // in-flight animations: prop -> {from, to, start, duration}. Transitions
127
+ // delete themselves as they land; a loop entry (`loop: true`) is removed
128
+ // by `_updateLoops` and by nothing else
129
+ this._anim = null;
130
+ // False until the first frame places this node (`absolutize`). Read by
131
+ // `_retarget`: a style can be re-resolved several times between
132
+ // construction and that first frame — the attach-time theme merge is the
133
+ // common one, replacing a detached resolution against the desktop
134
+ // palette with one against the app's own — and none of those is a
135
+ // *change* the user saw, so no transition may start from it.
136
+ this._placed = false;
137
+ // the loops this node's style declares, whether or not they are running
138
+ this._loops = null;
139
+ // `resolvedTextStyle()`'s cache: this node's own text style over what it
140
+ // inherits. Undefined means "never asked", which is load-bearing — see
141
+ // `_retext`
142
+ this._resolvedText = undefined;
143
+ // `inheritedTextStyle`'s cache at a *scale boundary* — a node whose
144
+ // `scale` prop puts it in a different unit from its parent, so the
145
+ // device size it inherits has to be re-expressed. Null everywhere else,
146
+ // which is every node in a tree with no `scale` prop in it.
147
+ this._textScaled = null;
148
+ // `direction`'s cache, same contract as `_resolvedText`'s: undefined means
149
+ // "never asked", which is what lets `_redirectSubtree` stop at a node
150
+ // nothing below has resolved through
151
+ this._direction = undefined;
152
+ // hot pointer-path caches (issue #188): the children in paint order,
153
+ // re-verified against the live children on every read, and the
154
+ // subtree's hit reach, invalidated through _clearHitBounds()
155
+ this._paintOrderCache = null;
156
+ this._hitBoundsCache = null;
157
+ // a `$token` the theme does not define, held for `commitMount` to throw
158
+ // on this node's own fiber — see `_tokenProblem`. Strict mode only.
159
+ // `null` is "commitMount is still to come", `false` is "it has been and
160
+ // gone", and an Error is one waiting for it
161
+ this._tokenError = null;
162
+ // `'@container …'` blocks (styles.js), on the nodes that carry them and
163
+ // null on every other node: the style the record was built for, the
164
+ // container names it asks about (`''` for the unnamed ones), the sizes
165
+ // and answers the blocks last resolved to, and the pin an oscillating
166
+ // design is held at — see WindowNode._resolveContainerQueries. Before
167
+ // `_syncStyle`, which reads and writes it.
168
+ this._cq = null;
169
+ // A position placed after layout (`sticky`, or one registered with
170
+ // `registerPosition`): the paint reach this node was left at by the last
171
+ // pass that placed it — where its pixels are, which the next placement
172
+ // claims when it moves them (WindowNode._placeNodes) — the request its
173
+ // style resolved to, cached per style, and the definition that threw,
174
+ // which is not asked again until the style names another. Null on every
175
+ // node without one.
176
+ this._placedShown = null;
177
+ this._placementCache = null;
178
+ this._placementFailed = null;
179
+ // A layout algorithm (`layout`, docs/styling.md "Custom layouts"): the
180
+ // host state on a node that arranges its children with one — its
181
+ // children are yoga trees of their own then, and it a measured leaf —
182
+ // and, on each of those children, where the last placement put its
183
+ // margin box from the host's border box, whether it is one of the
184
+ // absolutely positioned ones, the handle the algorithm is given for it
185
+ // and its `layoutItem` resolved. Null everywhere else.
186
+ this._host = null;
187
+ this._hostSlot = null;
188
+ this._hostAbsolute = false;
189
+ this._handle = null;
190
+ this._itemCache = null;
191
+ // …and the sizes the algorithm was told it takes, for as long as nothing
192
+ // inside it changes — it asks the same questions every run, several runs
193
+ // a frame — and whether a placement ever gave it less height than its
194
+ // content, which from then on keeps its height floors measured
195
+ this._hostSizes = null;
196
+ this._hostSqueezed = false;
197
+ // the rect its tree was last laid out at by a placement, while nothing
198
+ // has laid it out since — the next placement at the same rect has
199
+ // nothing to do
200
+ this._hostLaidAt = null;
201
+ // the layout that threw, with the style value that named it: flexbox
202
+ // until the style names a different one
203
+ this._layoutAbandoned = null;
204
+ this._syncStyle(props);
205
+ this.yoga = yoga ? createLayoutNode() : null;
206
+ if (this.yoga) {
207
+ applyLayoutDefaults(this.yoga);
208
+ applyLayoutStyle(this.yoga, this.style);
209
+ // An element with a size of its own says so by implementing
210
+ // `measureContent`, and the base class is what wires it to layout —
211
+ // so a third-party element reaches everything that asks a leaf for its
212
+ // size, including the content floor `minWidth: 'auto'` is measured
213
+ // with (#248), without knowing either of them exists.
214
+ if (typeof this.measureContent === 'function') this._useMeasureContent();
215
+ if (this.style.layout != null || this.style.display === 'grid') {
216
+ this._syncLayoutHost();
217
+ }
218
+ }
219
+ // the document selection: the state when this element is a `selectable`
220
+ // surface, and the part of somebody else's that lands on this one
221
+ this._textSelection = null;
222
+ this._selRange = null;
223
+ // an element with a selection of its own — `<textinput>` — whose subtree
224
+ // a document around it skips whole rather than lighting up half of what
225
+ // the user is editing
226
+ this.hasOwnSelection = false;
227
+ this._syncSelectable(props);
228
+ if (DEV) devCheckA11yProps(this);
229
+ }
230
+
231
+ get isWindow() {
232
+ return this.kind === 'window';
233
+ }
234
+
235
+ /**
236
+ * Prop names whose damage this element's own `applyProps` claims, and
237
+ * which `paintChanged` therefore does not claim the whole node for.
238
+ *
239
+ * Empty for everything that has not said otherwise, which is what keeps
240
+ * the default conservative. Registered elements declare theirs to
241
+ * `registerElement`, so the common case needs no subclass; an element
242
+ * whose answer depends on the *values* rather than the names overrides
243
+ * `paintChanged` instead.
244
+ */
245
+ get selfDamagedProps() {
246
+ return CUSTOM_SELF_DAMAGED.get(this.kind) ?? NO_SELF_DAMAGED;
247
+ }
248
+
249
+ /** Number of yoga-bearing children before `index` (window children and
250
+ * text spans/chunks do not join the parent's yoga tree). */
251
+ _yogaIndexAt(index) {
252
+ // A list of ordinary boxes — a scroll pane's rows, which is the list
253
+ // this is asked about a hundred times in one commit — has every child in
254
+ // the yoga tree, and then the yoga index *is* the child index. Counting
255
+ // the exceptions as they arrive turns that answer into a read instead of
256
+ // a walk of every sibling in front of the new row (issue #397).
257
+ if (this._nonYogaKids === 0) return index;
258
+ let n = 0;
259
+ for (let i = 0; i < index; i++) {
260
+ if (this._joinsYoga(this.children[i])) n++;
261
+ }
262
+ return n;
263
+ }
264
+
265
+ _joinsYoga(child) {
266
+ return Boolean(
267
+ this.yoga && child.yoga && !child.isWindow && this._host === null,
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Where `child` sits in `this.children`.
273
+ *
274
+ * The cached slot is checked rather than trusted: a node appears in the
275
+ * list once, so `children[i] === child` *is* the proof that `i` is its
276
+ * index, and a cache that has gone stale costs a scan rather than a wrong
277
+ * answer. `_spliceChild` refreshes the two slots it knows — the child it
278
+ * placed and the sibling it pushed along — which is what keeps a run of
279
+ * inserts in front of the same trailing sibling (every virtualized list's
280
+ * commit) off the scan entirely.
281
+ */
282
+ _indexOfChild(child) {
283
+ if (this.children[child._childIndex] === child) return child._childIndex;
284
+ const i = this.children.indexOf(child);
285
+ child._childIndex = i;
286
+ return i;
287
+ }
288
+
289
+ appendChild(child) {
290
+ this.insertBefore(child, null);
291
+ }
292
+
293
+ /** Splice `child` in front of `beforeChild` (end of the list when that is
294
+ * null), first taking it out of its old slot: React reorders a keyed list
295
+ * by calling insertBefore with a child that is *already* mounted here, and
296
+ * without the removal it would appear twice. Returns the new index. */
297
+ _spliceChild(child, beforeChild) {
298
+ // `parent === this` is the cheap form of "already in this list" — the
299
+ // two are set and cleared together — so a child arriving for the first
300
+ // time, which is every node of a freshly mounted subtree, pays no scan
301
+ // at all for the question.
302
+ const from = child.parent === this ? this._indexOfChild(child) : -1;
303
+ if (from !== -1) this.children.splice(from, 1);
304
+ else if (outsideYoga(child)) this._nonYogaKids++;
305
+ const before = beforeChild == null ? -1 : this._indexOfChild(beforeChild);
306
+ const index = before === -1 ? this.children.length : before;
307
+ this.children.splice(index, 0, child);
308
+ // The two slots this splice knows. Every other cached index at or after
309
+ // `index` has shifted by one and will be caught by the check in
310
+ // `_indexOfChild`; these two are the ones a run of inserts in front of
311
+ // the same sibling asks about again on the very next call.
312
+ child._childIndex = index;
313
+ if (beforeChild != null) beforeChild._childIndex = index + 1;
314
+ return index;
315
+ }
316
+
317
+ insertBefore(child, beforeChild) {
318
+ if (child.isPopup) {
319
+ // popups live anywhere in the JSX tree but are independent
320
+ // override-redirect windows: bookkeeping only, no yoga, no paint —
321
+ // but they do inherit the theme of where they are written
322
+ const mounting = child.parent == null;
323
+ this._spliceChild(child, beforeChild);
324
+ child.parent = this;
325
+ if (this.theme || child.props.theme) child._themeChanged(mounting);
326
+ a11yHooks.attached?.(this, child);
327
+ return;
328
+ }
329
+ if (child.isWindow) {
330
+ throw new Error(
331
+ `react-x11: <window> cannot be nested inside <${this.kind}>; ` +
332
+ 'windows may only appear at the root or inside another <window>.',
333
+ );
334
+ }
335
+ // A registered element that declared childrenAllowed: false says so
336
+ // here, rather than laying out a child that will never paint. The flag
337
+ // is set on the instance by the registry, so this stays one property
338
+ // read and src/nodes/ keeps not importing it.
339
+ if (this._childrenAllowed === false) {
340
+ throw new Error(
341
+ `react-x11: <${this.kind}> takes no children (registered with ` +
342
+ `childrenAllowed: false), but <${child.kind}> is inside it.`,
343
+ );
344
+ }
345
+ // A node's size comes from its measure function or from its children,
346
+ // never both — and yoga does not merely refuse the second one, it aborts
347
+ // the WebAssembly module, which takes the process down naming nothing
348
+ // the developer wrote. The built-ins reach it too: `<text>` is turned
349
+ // away earlier by `createInstance`, which knows what its content is, but
350
+ // `<image>`, `<svg>`, `<textinput>` and `<textarea>` arrive here.
351
+ if (this._measureFn && this._joinsYoga(child)) {
352
+ throw new Error(
353
+ `react-x11: <${this.kind}> measures its own content, so it cannot ` +
354
+ `contain <${child.kind}> — layout sizes such an element from its ` +
355
+ `measure function and gives its children no say. Render <${child.kind}> ` +
356
+ `beside it rather than inside it; or, if <${this.kind}> is meant to ` +
357
+ 'arrange children, remove its measureContent() and let flexbox size ' +
358
+ 'it from what is inside it.',
359
+ );
360
+ }
361
+ // captured before the child joins, so it covers the arrangement that is
362
+ // about to be replaced (see _childListChanged). A viewport mid-blit has
363
+ // nothing vacating — the child being added had no pixels — and the
364
+ // layout diff claims where it lands, so it names no region at all.
365
+ const before = this._blitLedgerOpen() ? null : this._childListBefore();
366
+ // a move has to leave the yoga tree too — yoga aborts on insertChild of
367
+ // a node that still has a parent
368
+ if (child.parent === this && this._joinsYoga(child)) {
369
+ this.yoga.removeChild(child.yoga);
370
+ }
371
+ // no parent means never attached: this insert is a mount, and the theme
372
+ // walk resolves without claiming — a keyed reorder arrives here too, with
373
+ // its parent still set, and that one keeps the claims (issue #402)
374
+ const mounting = child.parent == null;
375
+ const index = this._spliceChild(child, beforeChild);
376
+ child.parent = this;
377
+ if (this._joinsYoga(child)) {
378
+ this.yoga.insertChild(child.yoga, this._yogaIndexAt(index));
379
+ } else if (this._host !== null && child.yoga && !child.isWindow) {
380
+ // a layout's child is a tree of its own; a keyed reorder only moves
381
+ // it in the list, which is the algorithm's to read
382
+ if (mounting) this._adoptHostChild(child);
383
+ this._markHostDirty();
384
+ }
385
+ child._setRoot(this.root);
386
+ child._registerSizeQueries();
387
+ // it can see its ancestors now, so any token in its style can resolve.
388
+ // With no theme anywhere there is nothing to resolve and nothing to walk
389
+ if (this.theme || child.props.theme) child._themeChanged(mounting);
390
+ // …and the same for the scale: a subtree styled while detached resolved
391
+ // against the app's, and only now can see the `scale` props above it.
392
+ child._rescaleSubtree(mounting);
393
+ this._textContentChanged();
394
+ this._childListChanged(before);
395
+ a11yHooks.attached?.(this, child);
396
+ }
397
+
398
+ /**
399
+ * This node's paint bounds from before a child-list mutation — the `before`
400
+ * half of `_childListChanged`'s protocol, captured while a departing child
401
+ * is still attached.
402
+ *
403
+ * Walked once per node per frame rather than once per mutation. A commit
404
+ * that mounts a virtualized list's window inserts a hundred rows into one
405
+ * pane, one `insertBefore` at a time, and a walk of the whole pane per row
406
+ * is what made that commit O(rows x pane) (issue #397).
407
+ *
408
+ * Reusing the first walk's answer is not an approximation. Nothing is laid
409
+ * out or painted between two mutations in the same frame, so every child
410
+ * still carries the rect it was last painted at, and a child that leaves
411
+ * later in the frame was in the list — and so inside the rect — when the
412
+ * first walk ran. `root._reflowed` is the marker for "this frame already
413
+ * has one", which is exactly its lifetime: joined at the first claim,
414
+ * cleared by `flush()`.
415
+ */
416
+ _childListBefore() {
417
+ const root = this.root;
418
+ // A subtree still being built off-tree claims nothing — this is the
419
+ // `appendInitialChild` path, which is most of a mount, and where the
420
+ // walk used to be thrown away by `_childListChanged`'s `!root` return.
421
+ if (!root) return null;
422
+ if (root._reflowed.has(this) && this._reflowBefore) {
423
+ return this._reflowBefore;
424
+ }
425
+ // NO_DAMAGE, not null, when a blitting viewport above clips this node
426
+ // away entirely (issue #398): null here would read as "somewhere" and
427
+ // repaint the window.
428
+ return (this._reflowBefore = this._claimBounds() ?? NO_DAMAGE);
429
+ }
430
+
431
+ /**
432
+ * A child was inserted or removed. `before` is this node's paint bounds from
433
+ * *before* the mutation, which the caller has to capture while the departing
434
+ * child is still attached.
435
+ *
436
+ * The damage is this subtree before the mutation unioned with the same
437
+ * subtree after layout. The second half is not measurable yet — an
438
+ * inserted child has no rect until layout runs — so the node is queued
439
+ * for the root to re-measure once it has. Siblings the reflow displaces
440
+ * outside this subtree (this node growing taller, say) claim themselves
441
+ * through the layout diff in flush(), which is what lets this claim stay
442
+ * bounded without requiring the node's own size to be pinned.
443
+ */
444
+ _childListChanged(before) {
445
+ // belt for a subtree attached imperatively with its rect already laid
446
+ // out — nothing then re-runs _assignAbs to notice the reach grew
447
+ this._clearHitBounds();
448
+ const root = this.root;
449
+ if (!root) return;
450
+ // A viewport keeping a ledger this frame (issue #398) says both halves
451
+ // of the protocol finer: `before` is the departing child's own rect
452
+ // rather than this node's box, and the "after" half comes from the
453
+ // shifted layout diff, which claims an entering child where it lands
454
+ // and says nothing about the ones that only rode the scroll. Joining
455
+ // `_reflowed` would undo both — its post-layout claim is this node's
456
+ // box, the whole band the blit is about to move.
457
+ if (this._blitLedgerOpen()) {
458
+ root.invalidate(true, before ?? NO_DAMAGE, 'child-list');
459
+ return;
460
+ }
461
+ root.invalidate(true, before, 'child-list');
462
+ root._reflowed.add(this);
463
+ }
464
+
465
+ removeChild(child) {
466
+ const index = this._indexOfChild(child);
467
+ if (index === -1) return;
468
+ // told while the child is still wired, so the bridge can compute the
469
+ // index the AT will see the removal at
470
+ a11yHooks.detach?.(this, child);
471
+ // captured while the child is still attached, so it covers the rect the
472
+ // child is about to stop occupying — the child's own, for a viewport
473
+ // mid-blit, where this node's box is the whole scrolled band
474
+ const before = this._blitLedgerOpen()
475
+ ? (child._claimBounds() ?? NO_DAMAGE)
476
+ : this._childListBefore();
477
+ this.children.splice(index, 1);
478
+ if (outsideYoga(child)) this._nonYogaKids--;
479
+ if (this._joinsYoga(child)) {
480
+ this.yoga.removeChild(child.yoga);
481
+ } else if (this._host !== null && child.yoga && !child.isWindow) {
482
+ if (child._hostAbsolute) {
483
+ this._host.absolute.removeChild(child.yoga);
484
+ child._hostAbsolute = false;
485
+ }
486
+ this._markHostDirty();
487
+ }
488
+ child.parent = null;
489
+ child.destroySubtree();
490
+ if (child.yoga && !child.isWindow) {
491
+ child.yoga.freeRecursive();
492
+ child.yoga = null;
493
+ }
494
+ this._textContentChanged();
495
+ this._childListChanged(before);
496
+ }
497
+
498
+ /** Destroy real resources (X windows) in this subtree. Yoga nodes are
499
+ * freed by the caller via freeRecursive on the subtree top. */
500
+ destroySubtree() {
501
+ this.destroyed = true;
502
+ // a loop outlives nothing: the window drops it from the set that keeps
503
+ // its frame clock alive, and stops watching visibility with the last one
504
+ this.root?._forgetLoopNode(this);
505
+ // …and out of the animating set in the same breath rather than on the
506
+ // next tick, so a spinner that unmounts leaves the clock idle even if
507
+ // nothing else ever asks for a frame
508
+ this.root?._animating.delete(this);
509
+ this.root?._opaqueNodes?.delete(this);
510
+ // a surface that goes away takes its selection with it, and the app-wide
511
+ // claim on being the one showing one goes with it too
512
+ this._textSelection?.destroy();
513
+ if (this.hasOwnSelection) dropVisibleSelection(this);
514
+ for (const child of this.children) child.destroySubtree();
515
+ // A layout host's children's trees are roots of their own, which the
516
+ // `freeRecursive` that takes this node's box does not reach.
517
+ if (this._host !== null) this._freeHostTrees();
518
+ }
519
+
520
+ _setRoot(root) {
521
+ if (this.root === root) return;
522
+ // a layout host is found through its window's registry, like a placed
523
+ // node
524
+ if (this._host !== null) {
525
+ this.root?._layoutHosts?.delete(this);
526
+ root?._layoutHosts?.add(this);
527
+ }
528
+ // an element answering `opaqueRect()` is one the window asks per pass
529
+ const opaque = this.opaqueRect !== Node.prototype.opaqueRect;
530
+ if (opaque) this.root?._opaqueNodes?.delete(this);
531
+ this.root = root;
532
+ if (opaque) root?._opaqueNodes?.add(this);
533
+ // styled before it had a window, so this is where a placed node is
534
+ // first registered (WindowNode._placeNodes)
535
+ if (this.style && isPlaced(this.style)) root?._placedNodes?.add(this);
536
+ // A node is styled in its constructor, before it has a window — so this
537
+ // is where a loop declared by the very first style finds a frame clock
538
+ // to run on.
539
+ if (this._loops) this._updateLoops();
540
+ for (const child of this.children) {
541
+ if (!child.isWindow) child._setRoot(root);
542
+ }
543
+ }
544
+
545
+ /** Called when descendant text content may have changed; overridden by
546
+ * TextNode, forwarded upward by spans/chunks. */
547
+ _textContentChanged() {}
548
+
549
+ applyProps(newProps, oldProps) {
550
+ const prev = this.props;
551
+ const prevStyle = this.style;
552
+ const themeChanged = newProps.theme !== prev.theme;
553
+ this.props = newProps;
554
+ if (themeChanged) this._themeChanged();
555
+ // Ahead of the `_syncStyle` below, because the funnel that runs
556
+ // multiplies by `this.scale` and this is what makes it read the new
557
+ // factor. The walk restyles this node too, so the call after it hits
558
+ // the identity check and costs nothing twice.
559
+ if (newProps.scale !== prev.scale) this._rescaleSubtree();
560
+ const style = this._syncStyle(newProps);
561
+ let layoutChanged = false;
562
+ // hoisted styles hit the identity check and skip the whole update
563
+ if (this.yoga && style !== prevStyle) {
564
+ layoutChanged = applyLayoutStyle(this.yoga, style, prevStyle);
565
+ }
566
+ if (Boolean(newProps.trapFocus) !== Boolean((oldProps ?? prev).trapFocus)) {
567
+ this._syncFocusScope();
568
+ }
569
+ // and so does being a selection surface
570
+ this._syncSelectable(newProps);
571
+ // drop-target registration follows the props edge, like trapFocus
572
+ if (hasDropProps(newProps) !== hasDropProps(oldProps ?? prev)) {
573
+ const root = this.root;
574
+ if (root?._registerDropTarget) {
575
+ if (hasDropProps(newProps)) root._registerDropTarget(this);
576
+ else root._forgetDropTarget(this);
577
+ }
578
+ }
579
+ // This is how every React update arrives, so it is where partial
580
+ // painting pays for itself. `applyLayoutStyle` has just told us whether
581
+ // anything can have *moved*: if so, this subtree's before/after rects
582
+ // plus the layout diff bound the frame; if not, this node's own region
583
+ // bounds what changed — a new colour, a new label, a different border.
584
+ // And if nothing it draws changed at all, it contributes no damage,
585
+ // which is what keeps a commit from widening the region to every node
586
+ // it touched.
587
+ if (layoutChanged) {
588
+ this._invalidateLayout('props');
589
+ } else {
590
+ // The style half is asked here rather than inside `paintChanged`, and
591
+ // stays core's answer: what a style change moves is the background,
592
+ // the border and the clip that `Node.paint` draws, so an element is
593
+ // not in a position to excuse one.
594
+ const styleChanged =
595
+ style !== prevStyle && paintPropsChanged(style, prevStyle);
596
+ this.root?.invalidate(
597
+ false,
598
+ styleChanged || this.paintChanged(newProps, prev) ? this : NO_DAMAGE,
599
+ 'props',
600
+ );
601
+ }
602
+ if (DEV) devCheckA11yProps(this);
603
+ a11yHooks.propsChanged?.(this);
604
+ }
605
+
606
+ /**
607
+ * Did anything this node *draws* change? Answering true damages the whole
608
+ * node; answering false contributes no damage at all.
609
+ *
610
+ * Deliberately conservative, because the cost of a wrong "no" is a stale
611
+ * pixel that nothing will come back to fix. A prop that is not equal to
612
+ * the one it replaced is "yes it changed", which is what makes the default
613
+ * safe without knowing about subclasses: `<image src>`, `<canvas onDraw>`,
614
+ * a `value`, a `placeholder`, a `caretColor` — any prop a subclass paints
615
+ * from is a prop, so a change to it lands here as an inequality and damages
616
+ * the node. Three kinds are skipped because they cannot affect this node's
617
+ * own drawing:
618
+ *
619
+ * - `children`, which the reconciler mutates through appendChild /
620
+ * removeChild / commitTextUpdate, each of which invalidates on its own;
621
+ * - event handlers, rebuilt every render and never painted;
622
+ * - `style`, compared by value by the caller — so a style object React
623
+ * rebuilt with the same contents costs nothing, which is the whole
624
+ * point, since React rebuilds sibling styles on every render and a
625
+ * commit would otherwise damage every node it walked.
626
+ *
627
+ * **The seam (issue #301).** "The node" is the wrong granularity for an
628
+ * element that draws a *scene*: a graph view handed a new `nodes` array
629
+ * every drag step has already claimed the box the dragged node moved
630
+ * through, and this answering "yes" over the top widens that to the whole
631
+ * pane and throws the scoped work away. Such an element either names those
632
+ * props in `selfDamagedProps` — the declarative form, and what
633
+ * `registerElement({ selfDamagedProps })` fills — or overrides this method
634
+ * when the answer depends on the values rather than the names:
635
+ *
636
+ * ```js
637
+ * paintChanged(next, prev) {
638
+ * // my own applyProps diffed these and claimed exactly what moved
639
+ * if (onlyPositionsMoved(next.nodes, prev.nodes)) return false;
640
+ * return super.paintChanged(next, prev); // everything else is core's
641
+ * }
642
+ * ```
643
+ *
644
+ * An override that answers wrong shows stale pixels, so the part it does
645
+ * not know about has to reach `super` — a new `aria-label`, a prop the
646
+ * element grows next year.
647
+ */
648
+ paintChanged(newProps, prev) {
649
+ const claimed = this.selfDamagedProps;
650
+ const keys = new Set([...Object.keys(newProps), ...Object.keys(prev)]);
651
+ for (const key of keys) {
652
+ if (key === 'children' || key === 'style' || isEventProp(key)) continue;
653
+ if (claimed.has(key)) continue;
654
+ if (newProps[key] !== prev[key]) return true;
655
+ }
656
+ return false;
657
+ }
658
+
659
+ setHidden(hidden) {
660
+ // claimed before the yoga flip so the bound covers the arrangement
661
+ // being vacated; the reveal is the after-layout re-claim
662
+ this._invalidateLayout('props');
663
+ this.hidden = hidden;
664
+ if (this.yoga) {
665
+ this.yoga.setDisplay(
666
+ hidden || this.style.display === 'none'
667
+ ? Yoga.DISPLAY_NONE
668
+ : Yoga.DISPLAY_FLEX,
669
+ );
670
+ }
671
+ this._visibilityChanged(!hidden);
672
+ }
673
+
674
+ /**
675
+ * Whether this subtree is on screen just changed, so focus has to follow
676
+ * it — released when it goes, handed back when it returns. The rule and
677
+ * the reasoning live on the focus manager (`subtreeHidden`, events.js);
678
+ * this is the funnel every route to it comes through: the `hidden` flag
679
+ * React sets for `<Suspense>`/`<Activity>`, and `display: 'none'` from a
680
+ * style, a state block or a size query (`_retarget`).
681
+ */
682
+ _visibilityChanged(visible) {
683
+ // Same rule, and the reason it shares this funnel: a loop inside a
684
+ // subtree that just went off the screen is drawing frames for nobody,
685
+ // whichever of the three routes hid it. Re-evaluated for the whole
686
+ // window rather than for this subtree — the set is the handful of nodes
687
+ // that declare an `animation`, and each one answers for itself.
688
+ if (this.root?._loopNodes?.size) this.root._refreshLoops();
689
+ const events = this._focusManager();
690
+ if (!events) return;
691
+ if (visible) events.subtreeRevealed(this);
692
+ else events.subtreeHidden(this);
693
+ }
694
+
695
+ /**
696
+ * Focus this node, as clicking it would: the owning window's focus moves
697
+ * here, `onBlur` fires on whatever had it, `onFocus` here. Also pulls the
698
+ * X input focus to the window if the window manager gave it away.
699
+ */
700
+ focus() {
701
+ this._focusManager()?.focus(this);
702
+ return this;
703
+ }
704
+
705
+ /** Give up focus, leaving the window with nothing focused. */
706
+ blur() {
707
+ const events = this._focusManager();
708
+ if (events?.focused === this) events.focus(null);
709
+ return this;
710
+ }
711
+
712
+ /** Whether this node has the owning window's focus. */
713
+ get focused() {
714
+ return this._focusManager()?.focused === this;
715
+ }
716
+
717
+ /** Whether focus is on this node or inside it — CSS `:focus-within`. A
718
+ * `<popup>` counts as inside the node it hangs off in the JSX tree, which
719
+ * is what a modal needs to know before taking focus itself. */
720
+ get focusWithin() {
721
+ const focused = this._focusManager()?.focused;
722
+ return Boolean(focused) && this.contains(focused);
723
+ }
724
+
725
+ /** Whether `node` is this node or a descendant of it (DOM `contains`). */
726
+ contains(node) {
727
+ for (let n = node; n; n = n.parent) {
728
+ if (n === this) return true;
729
+ }
730
+ return false;
731
+ }
732
+
733
+ /**
734
+ * The text this element reports through `a11yTextState()` may have moved
735
+ * — an edit, a caret move, a selection change, a composition (#257). The
736
+ * same notification `<textinput>`'s `_repaint` makes, and the reason an
737
+ * assistive technology hears a third-party editor at all: the state is
738
+ * *pulled* when this says it is worth pulling.
739
+ *
740
+ * Free when nobody is listening — one property read, the hook slots being
741
+ * null until a bridge or the test spy fills them — so an element may call
742
+ * it on every edit without asking whether accessibility is on.
743
+ */
744
+ notifyA11yTextChanged() {
745
+ a11yHooks.textState?.(this);
746
+ }
747
+
748
+ /**
749
+ * The scene this element reports through `a11yScene()` has changed — an
750
+ * item added or removed, one selected, the element's own cursor moved
751
+ * onto another one (#304). The children an assistive technology is
752
+ * holding are re-read and the difference announced.
753
+ *
754
+ * A scene that is a function of the props needs no call: a commit already
755
+ * re-reads it. This is for everything the element does on its own —
756
+ * a drag, an animation, its own arrow keys.
757
+ *
758
+ * Free when nobody is listening, the same one property read
759
+ * `notifyA11yTextChanged()` costs.
760
+ */
761
+ notifyA11ySceneChanged() {
762
+ a11yHooks.propsChanged?.(this);
763
+ }
764
+
765
+ /** Where focus for this node lives: its own window's EventManager, or —
766
+ * inside a `<popup>`, which never receives the X input focus — the owner
767
+ * window's (see EventManager.focusManager). */
768
+ _focusManager() {
769
+ return this.root?.events?.focusManager ?? null;
770
+ }
771
+
772
+ /** Register or drop this node's focus scope to match the `trapFocus` prop.
773
+ * Idempotent: called at mount (commitMount) and on every prop update. */
774
+ _syncFocusScope() {
775
+ const events = this._focusManager();
776
+ if (!events) return;
777
+ if (this.props.trapFocus) events.pushScope(this);
778
+ else events.popScope(this);
779
+ }
780
+
781
+ // The pointer and the keys a selection is made with. They are default
782
+ // actions on the *base* class because the press lands on whatever is under
783
+ // the pointer — a `<text>`, an `<image>`, the gap between two paragraphs —
784
+ // and every one of them has to reach the surface above it. An element that
785
+ // takes presses of its own overrides these and is, by that alone, not part
786
+ // of a document; one that wants both calls `super`.
787
+ defaultMouseDown(ev) {
788
+ selectionSurfaceOf(this)?.press(ev);
789
+ }
790
+
791
+ defaultMouseDrag(ev) {
792
+ selectionSurfaceOf(this)?.drag(ev);
793
+ }
794
+
795
+ defaultMouseUp(ev) {
796
+ selectionSurfaceOf(this)?.release(ev);
797
+ }
798
+
799
+ /**
800
+ * The selection keys, and then **Space or Enter on anything clickable**.
801
+ *
802
+ * A focusable node with an `onClick` used to take focus, draw a ring, be
803
+ * reachable by Tab and be activatable by a screen reader — and do nothing
804
+ * at all when the keyboard pressed it (issue #329). It looked operable and
805
+ * was not, which is the failure mode a focus ring makes *worse*: the ring
806
+ * is a promise. Every control an application builds out of a `<box>`
807
+ * rather than out of `Button` had it, silently.
808
+ *
809
+ * It is the click itself, not a second definition of one: `synthesizeClick`
810
+ * is the function an AT's `DoAction("activate")` already went through, so
811
+ * a control that acts on the press hears the press either way and the two
812
+ * paths cannot drift. The rule for *what* is activatable is the same one
813
+ * the bridge writes down — there is an `onClick` here — minus the bridge's
814
+ * role clause, which advertises an action to something that cannot press a
815
+ * key (a11y.js, `hasClickHandler`).
816
+ *
817
+ * **One key rule, no role table.** The web gives `checkbox` Space and not
818
+ * Enter, and a link Enter and not Space, because on the web those keys are
819
+ * already spoken for — Space scrolls the page, Enter submits the form.
820
+ * Neither is true here: a default action runs on the focused node, so the
821
+ * scroll pane a row sits in never sees the row's Space, and there is no
822
+ * implicit submit. All a role table could buy, then, is *fewer* keys
823
+ * working on a control that draws a focus ring — which is the bug.
824
+ *
825
+ * The two ways out, both ordinary: `preventDefault()` in the element's own
826
+ * `onKeyDown` (the seam an application uses — a `<box>` that wants Enter
827
+ * for something else), and overriding this method (the seam an element
828
+ * uses). A scroll pane that is *itself* clickable takes the third: its
829
+ * `defaultKeyDown` answers Space with a page and never reaches here, so
830
+ * paging keeps the key it has always had and Enter activates.
831
+ */
832
+ defaultKeyDown(ev) {
833
+ this._textSelection?.keyDown(ev);
834
+ if (ev.defaultPrevented) return;
835
+ const enter = ev.keysym === XK_RETURN || ev.keysym === XK_KP_ENTER;
836
+ // Space by either name: `XK_space` *is* code point 32 — a Latin-1 keysym
837
+ // and its character are the same number — and both fields are read
838
+ // because a synthetic event may carry only one of them, the way the
839
+ // scroll keys next door read the keysym and every widget read the code
840
+ // point. A key an open composition took reaches no default action at all.
841
+ const space = ev.keysym === XK_SPACE || ev.codepoint === 32;
842
+ if (!enter && !space) return;
843
+ if (!hasClickHandler(this)) return;
844
+ // consumed, said the way every default action says it: what it prevents
845
+ // is the default action after this one
846
+ ev.preventDefault();
847
+ synthesizeClick(this, this.abs, ev.nativeEvent);
848
+ }
849
+ }
850
+
851
+ // The rest of Node's methods live with the concern they serve, one file
852
+ // per concern; install.js explains the arrangement.
853
+ installMethods(
854
+ Node,
855
+ NodeStyling,
856
+ NodeCascade,
857
+ NodeQueries,
858
+ NodeAnimation,
859
+ NodeLayout,
860
+ NodeLayoutHost,
861
+ NodePosition,
862
+ NodeHitTest,
863
+ NodeInvalidate,
864
+ NodeScrollBlit,
865
+ NodePaint,
866
+ NodeBoxPaint,
867
+ NodeSelectable,
868
+ );