react-x11 2.11.0 → 2.13.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 +12 -4
  3. package/src/Reconciler.js +19 -31
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/appcontext.js +59 -30
  7. package/src/bootstrap.js +14 -0
  8. package/src/clientmessage.js +1 -1
  9. package/src/cocoa/app.js +303 -49
  10. package/src/cocoa/bezels.js +175 -30
  11. package/src/cocoa/dnd.js +27 -13
  12. package/src/cocoa/fonts.js +3 -3
  13. package/src/cocoa/glarea.js +24 -5
  14. package/src/cocoa/main.d.ts +8 -0
  15. package/src/cocoa/main.js +43 -0
  16. package/src/cocoa/overlay.js +159 -0
  17. package/src/cocoa/panehost.js +15 -5
  18. package/src/cocoa/presenter.js +13 -9
  19. package/src/cocoa/promotion.js +17 -7
  20. package/src/cocoa/relaunch.js +207 -0
  21. package/src/cocoa/threaded.js +246 -0
  22. package/src/cocoa/window.js +256 -42
  23. package/src/components/Select.js +2 -2
  24. package/src/components/anchor.js +3 -3
  25. package/src/components/native.js +12 -7
  26. package/src/components/theme.js +2 -2
  27. package/src/debug.js +1 -1
  28. package/src/decorations.js +1 -1
  29. package/src/editmenu.js +2 -2
  30. package/src/embedding.js +31 -0
  31. package/src/errors.js +46 -0
  32. package/src/events.js +78 -18
  33. package/src/foreignnodes.js +59 -5
  34. package/src/frames.js +2 -2
  35. package/src/glnodes.js +172 -41
  36. package/src/gloverlay.js +383 -0
  37. package/src/grid.js +1653 -0
  38. package/src/host.d.ts +230 -1
  39. package/src/host.js +11 -3
  40. package/src/imagesource.js +1 -1
  41. package/src/index.d.ts +34 -4
  42. package/src/index.js +9 -1
  43. package/src/layouts.js +721 -0
  44. package/src/node.d.ts +16 -3
  45. package/src/node.js +19 -21
  46. package/src/nodes/animation.js +644 -0
  47. package/src/nodes/box.js +21 -0
  48. package/src/nodes/boxpaint.js +473 -0
  49. package/src/nodes/canvas.js +269 -0
  50. package/src/nodes/cascade.js +600 -0
  51. package/src/nodes/damage.js +183 -0
  52. package/src/nodes/edithistory.js +124 -0
  53. package/src/nodes/editmenupopup.js +260 -0
  54. package/src/nodes/hittest.js +185 -0
  55. package/src/nodes/image.js +266 -0
  56. package/src/nodes/install.js +75 -0
  57. package/src/nodes/invalidate.js +465 -0
  58. package/src/nodes/kinds.js +31 -0
  59. package/src/nodes/layout.js +439 -0
  60. package/src/nodes/layouthost.js +949 -0
  61. package/src/nodes/node.js +868 -0
  62. package/src/nodes/paint.js +466 -0
  63. package/src/nodes/position.js +366 -0
  64. package/src/nodes/preedit.js +127 -0
  65. package/src/nodes/queries.js +330 -0
  66. package/src/nodes/rects.js +102 -0
  67. package/src/nodes/scrollable.js +891 -0
  68. package/src/nodes/scrollbars.js +138 -0
  69. package/src/nodes/scrollblit.js +1034 -0
  70. package/src/nodes/selectable.js +142 -0
  71. package/src/nodes/styling.js +225 -0
  72. package/src/nodes/text.js +649 -0
  73. package/src/nodes/textarea.js +391 -0
  74. package/src/nodes/textinput.js +1146 -0
  75. package/src/nodes/util.js +17 -0
  76. package/src/nodes/window/anchoring.js +161 -0
  77. package/src/nodes/window/capabilities.js +190 -0
  78. package/src/nodes/window/debugpaint.js +83 -0
  79. package/src/nodes/window/droptarget.js +145 -0
  80. package/src/nodes/window/floors.js +577 -0
  81. package/src/nodes/window/flush.js +369 -0
  82. package/src/nodes/window/hints.js +482 -0
  83. package/src/nodes/window/listeners.js +222 -0
  84. package/src/nodes/window/popup.js +71 -0
  85. package/src/nodes/window/size.js +591 -0
  86. package/src/nodes/window/window.js +954 -0
  87. package/src/palette.js +1 -1
  88. package/src/registry.js +7 -3
  89. package/src/styles.js +137 -15
  90. package/src/svgnodes.js +2 -1
  91. package/src/testing/harness.js +2 -2
  92. package/src/textselection.js +5 -3
  93. package/src/trace-registry.js +1 -1
  94. package/src/types/components.d.ts +38 -6
  95. package/src/types/elements.d.ts +26 -14
  96. package/src/types/nodes.d.ts +17 -2
  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,465 @@
1
+ // Invalidation: what a node damages when it changes, and how far its paint
2
+ // reaches (paint bounds, the opaque rect). WindowNode's half is the other
3
+ // end: how a window takes that damage in and clamps it to a frame.
4
+
5
+ import {
6
+ FULL_DAMAGE,
7
+ NO_DAMAGE,
8
+ DAMAGE_SLOP,
9
+ NO_BOUNDS_CACHE,
10
+ INVALIDATE_REASONS,
11
+ EMPTY_REASONS,
12
+ MAX_DAMAGE_RECTS,
13
+ addDamageRect,
14
+ damageToPaint,
15
+ } from './damage.js';
16
+ import { insetRect, unionRect, rectsBounds, rectsOverlap } from './rects.js';
17
+ import { BLIT_POISONED } from './scrollblit.js';
18
+ import { DEV } from './util.js';
19
+ import { debugPaint } from './window/debugpaint.js';
20
+
21
+ /** Node's half of invalidation, installed onto `Node.prototype` by node.js. */
22
+ export class NodeInvalidate {
23
+ /**
24
+ * Ask the owning window to repaint. The damage lives on the window node,
25
+ * which is the only node with a frame clock — this forwards there, so an
26
+ * element says `this.invalidate(false, this, 'props')` and never has to
27
+ * know that. Overridden by WindowNode, which *is* the collector.
28
+ *
29
+ * `damage` is the node or rect that changed. Passing one is the difference
30
+ * between repainting a control and repainting the window, and `this` is
31
+ * almost always the right answer (docs/extending.md). Before the node is
32
+ * attached there is no window and nothing on screen, so this is a no-op —
33
+ * the mount invalidates in full anyway.
34
+ */
35
+ invalidate(layoutChanged = false, damage = null, reason = null) {
36
+ // a layout change may grow what an enclosing scroll pane has to scroll,
37
+ // through a route yoga never sees (issue #405)
38
+ if (layoutChanged) this._markScrollMeasureDirty();
39
+ // a node that says its appearance changed may have changed how far it
40
+ // reaches — a shadow, an outline, a scene element's ink — so its cached
41
+ // paint reach goes with the claim
42
+ if (damage === this || damage === null) this._clearPaintBounds();
43
+ this.root?.invalidate(layoutChanged, damage, reason);
44
+ }
45
+
46
+ /**
47
+ * The rect this paint pass is repainting, or null when it is repainting
48
+ * the whole window — and null outside a paint, which reads the same way:
49
+ * nothing is bounding you, so draw everything (issue #301).
50
+ *
51
+ * The other end of `invalidate`. A frame repaints one damage rect per
52
+ * pass, clipped to it and with whole subtrees outside it culled, so an
53
+ * element whose node *is* one node — a `<box>`, a `<text>` — never needs
54
+ * this: being painted at all already means it is inside. An element that
55
+ * draws a **scene** into one node does: without it, a `<flow>` handed a
56
+ * pass over the 80×40 box a dragged node moved through redraws all three
57
+ * hundred nodes, all seven hundred edges, the grid and the minimap into a
58
+ * clip that throws almost all of it away. With it, the element culls the
59
+ * same way core culls the tree.
60
+ *
61
+ * Window coordinates, the same space as `abs`, `contentBox()` and an
62
+ * event's `x`/`y`. Read-only, like `abs`: it is this frame's own rect and
63
+ * the clip is already set from it.
64
+ *
65
+ * Never inside `paintCached`, which draws into a surface in its own
66
+ * coordinates: a cached copy culled against the window's damage is stored
67
+ * half-drawn under a key claiming it is whole, and every later frame that
68
+ * hits the key gets the hole.
69
+ */
70
+ paintDamage() {
71
+ return this.root?._paintDamage ?? null;
72
+ }
73
+
74
+ /**
75
+ * The rect this element writes opaque pixels over on every paint — in
76
+ * window coordinates like `abs`, in whole pixels — or null, the default,
77
+ * which promises nothing.
78
+ *
79
+ * What it buys: a pass that lies inside it is painted without the fills
80
+ * that would be under it — the window's background, this node's own and
81
+ * every ancestor's — because not one of those pixels survives. On the
82
+ * Cocoa backend those fills are full-area CoreGraphics passes, and for a
83
+ * streaming terminal they were a fifth of the frame; on X11 they are
84
+ * composites the server ran for nothing. An element with a retained
85
+ * surface it draws whole — a terminal, a media frame, a chart — answers
86
+ * with the rect it covers, and claims its damage as a **rect inside it**
87
+ * rather than as the node: a node claim is inflated by a pixel of slop,
88
+ * which is outside the rect and so never covered.
89
+ *
90
+ * The promise is the element's to keep: every pixel of the rect, alpha
91
+ * one, on every paint of this node, whatever the props. A translucent
92
+ * element, or one that draws a background only sometimes, answers null.
93
+ * The answer is read at paint time, so it may follow `contentBox()`, and
94
+ * a fractional edge is not opaque — core takes the whole pixels inside.
95
+ */
96
+ opaqueRect() {
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * A layout-affecting change confined to this node: claim the subtree as
102
+ * it stands now, and queue it for a second claim once layout has run —
103
+ * the same before/after protocol `_childListChanged` uses. Anything
104
+ * *else* the reflow displaces claims itself through the layout diff in
105
+ * `flush()`, so the frame stays bounded instead of collapsing to
106
+ * FULL_DAMAGE the way a bare `invalidate(true, null)` would.
107
+ */
108
+ _invalidateLayout(reason) {
109
+ this._markScrollMeasureDirty();
110
+ const root = this.root;
111
+ if (!root) return;
112
+ // Same walk, same frame, same answer — see `_childListBefore`, whose
113
+ // record this shares so that a reflow and a child-list change on one
114
+ // node in one frame walk the subtree once between them.
115
+ root.invalidate(true, this._childListBefore(), reason);
116
+ root._reflowed.add(this);
117
+ }
118
+
119
+ /**
120
+ * The region this node can put ink in: its own rect unioned with every
121
+ * descendant's. Not the same as `abs` — a child of a node that does not
122
+ * clip may stick out of it (absolute positioning, a negative margin), and
123
+ * culling a subtree by the parent's rect alone would drop that child's
124
+ * paint. Recomputed on demand rather than cached in `absolutize`, because
125
+ * it is only ever asked for on the handful of nodes that invalidate.
126
+ */
127
+ paintBounds() {
128
+ const bounds = this._subtreeBounds();
129
+ // inflated once, here — doing it inside the recursion would compound the
130
+ // slop by one pixel per level of nesting
131
+ return {
132
+ x: bounds.x - DAMAGE_SLOP,
133
+ y: bounds.y - DAMAGE_SLOP,
134
+ width: bounds.width + DAMAGE_SLOP * 2,
135
+ height: bounds.height + DAMAGE_SLOP * 2,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * How far this node's drawing actually reaches, itself and its descendants.
141
+ *
142
+ * A node that clips its children ends the walk at its own rect: whatever
143
+ * they do beyond it never reaches the surface, so counting it would inflate
144
+ * every bound built from here. That matters most for a scrolling box, whose
145
+ * content is routinely thousands of pixels taller than the viewport — and
146
+ * can be ninety thousand pixels away mid-scroll (see `_offscreen`). Without
147
+ * this, damage claimed for a scrolled subtree covers the content extent
148
+ * instead of the viewport, and culling tests against a rect that misses
149
+ * almost nothing.
150
+ */
151
+ _subtreeBounds() {
152
+ // Cached like the hit reach (`_hitBounds`), and for the same reason: a
153
+ // bounded frame asks every subtree on the way to its rect whether it
154
+ // reaches in, and answering by walking the subtree made a one-cell
155
+ // repaint cost the whole tree — a millisecond at four thousand nodes,
156
+ // five at fourteen thousand, every frame. The cache is dropped up the
157
+ // chain by whatever changes a reach: a rect assigned by layout, a
158
+ // child list mutation (`_clearHitBounds`), and every change a node
159
+ // announces about itself (`invalidate`, `setStyleState`) — so a stale
160
+ // answer would need a change nobody announced, which is already a
161
+ // repaint bug.
162
+ const cached = this._paintBoundsCache;
163
+ if (cached && !NO_BOUNDS_CACHE) return cached;
164
+ let bounds = this._ownPaintBounds();
165
+ if (!this.clipsChildren()) {
166
+ for (const child of this.children) {
167
+ if (child.isWindow || !child.yoga || child.hidden) continue;
168
+ if (child.style?.display === 'none') continue;
169
+ bounds = unionRect(bounds, child._subtreeBounds());
170
+ }
171
+ }
172
+ this._paintBoundsCache = bounds;
173
+ return bounds;
174
+ }
175
+
176
+ /**
177
+ * This node's paint reach changed: drop the cached union here and up the
178
+ * chain, stopping where `_clearHitBounds` stops and for the same reasons
179
+ * — a clipping ancestor's reach is its own rect, and an ancestor already
180
+ * cleared has cleared the rest of the way up.
181
+ */
182
+ _clearPaintBounds() {
183
+ this._paintBoundsCache = null;
184
+ for (let n = this.parent; n; n = n.parent) {
185
+ if (n.clipsChildren() || n._paintBoundsCache === null) return;
186
+ n._paintBoundsCache = null;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * This node's own rect, grown by anything it draws outside it — the
192
+ * outline and the shadow, the only two. Per node rather than once at the
193
+ * top, because either can belong to any node and the bound has to cover it
194
+ * wherever it is; and the outline is counted even when the ring is
195
+ * currently *off*, because the frame that erases it is claimed after the
196
+ * state has already flipped back.
197
+ *
198
+ * A shadow cannot afford that trick — its extent is whatever the style
199
+ * says rather than a theme constant, so inflating for one that is not
200
+ * there would widen the claim of every node that has ever hovered. The
201
+ * frame that *removes* a shadow claims the old extent from `_retarget`
202
+ * instead, where both the old style and the new one are in hand.
203
+ */
204
+ _ownPaintBounds() {
205
+ const extent = Math.max(this._outlineExtent(), this._shadowExtent());
206
+ if (extent <= 0) return this.abs;
207
+ return {
208
+ x: this.abs.x - extent,
209
+ y: this.abs.y - extent,
210
+ width: this.abs.width + extent * 2,
211
+ height: this.abs.height + extent * 2,
212
+ };
213
+ }
214
+ }
215
+
216
+ /** WindowNode's half of invalidation, installed onto `WindowNode.prototype` by window/window.js. */
217
+ export class WindowInvalidate {
218
+ /**
219
+ * Mark the window as needing work before the next frame.
220
+ *
221
+ * `damage` is an optional node whose *appearance* changed, and it is what
222
+ * turns a full-window repaint into a partial one: the frame then repaints
223
+ * only the region that node covers, and skips emitting drawing for
224
+ * everything outside it. Two rules keep that safe:
225
+ *
226
+ * - a layout change gets no damage bound. Layout can move anything, and
227
+ * a node that moved leaves stale pixels behind at its old rect, which
228
+ * the new rect does not cover;
229
+ * - a caller that names no node means "something, somewhere", so it also
230
+ * repaints in full. Partial painting is therefore opt-in per call
231
+ * site, and forgetting to pass a node costs speed rather than
232
+ * correctness.
233
+ *
234
+ * `reason` is one word from INVALIDATE_REASONS saying *why* — purely
235
+ * diagnostic, collected per frame into `_lastReasons` so the frame log,
236
+ * REACT_X11_DEBUG_PAINT=full and the tracer can attribute a repaint.
237
+ * Omitting it costs nothing but attribution.
238
+ */
239
+ invalidate(layoutChanged, damage = null, reason = null) {
240
+ if (this.destroyed || !this.window) return;
241
+ if (!layoutChanged && damage === NO_DAMAGE) {
242
+ // Nothing this node draws changed, so it contributes no region — and
243
+ // contributing *nothing* is not the same as contributing "unknown".
244
+ // Returning before `needsPaint` is what makes the difference: a commit
245
+ // in which every node says this schedules no frame at all, where
246
+ // falling through would have marked the window dirty with no region
247
+ // recorded and so repainted all of it. That is the common case for a
248
+ // React re-render whose output is identical — hovering a control whose
249
+ // hover state it does not actually use, for instance. Whoever did
250
+ // change records its own region and schedules its own frame.
251
+ return;
252
+ }
253
+ if (reason) {
254
+ if (DEV && !INVALIDATE_REASONS.has(reason)) {
255
+ console.warn(
256
+ `react-x11: invalidate() got unknown reason ${JSON.stringify(reason)}`,
257
+ );
258
+ }
259
+ (this._frameReasons ??= new Set()).add(reason);
260
+ }
261
+ // A retained presenter keeps a per-node diff instead of damage rects,
262
+ // and this is the one channel every change already announces itself on
263
+ // (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
264
+ // ntk window has no ear here and the X11 path is byte-identical. A
265
+ // presenter that answers `true` has taken the claim onto a layer of its
266
+ // own (src/cocoa/promotion.js): the bitmap owes nothing for it, and the
267
+ // frame that is still owed — for the presenter's half, `prepareFrame` —
268
+ // paints nothing unless something else claims.
269
+ const taken =
270
+ this.window?.noteInvalidate?.(damage, layoutChanged, reason) === true;
271
+ if (taken && !layoutChanged) {
272
+ this._damage ??= [];
273
+ this.needsPaint = true;
274
+ this._scheduleFrame();
275
+ return;
276
+ }
277
+ if (layoutChanged) {
278
+ this.needsLayout = true;
279
+ // The content floors are measured from the tree, so anything that
280
+ // changed it has to give them up — and **scrolling does not**, which is
281
+ // the whole reason this is not just `needsLayout`: a scroll moves an
282
+ // offset applied during `absolutize` and leaves every yoga node exactly
283
+ // as it was, at input rate, on the biggest trees in any app.
284
+ if (reason !== 'scroll') this._floorsDirty = true;
285
+ if (reason !== 'scroll' && reason !== 'resize') {
286
+ this._floorsContentDirty = true;
287
+ }
288
+ }
289
+ // A layout change with no bound named repaints everything, because a
290
+ // reflow can move any node and one that moved leaves stale pixels at a
291
+ // rect its new position does not cover. Naming a node alongside
292
+ // `layoutChanged` is an assertion by the caller that the change is
293
+ // confined to that node's subtree *and* that the node clips its children,
294
+ // so both the old and the new position of anything that moved are inside
295
+ // the bound. Scrolling is the case that matters: it reflows a viewport's
296
+ // contents and nothing else, and it happens at input rate.
297
+ if (!damage && this._damage !== FULL_DAMAGE && debugPaint === 'full') {
298
+ // This call is what makes the coming frame unbounded, so this stack —
299
+ // not flush's — is the one that answers "who repainted the window".
300
+ // Captured only under the debug switch: stacks are not free.
301
+ this._fullRepaintCause = {
302
+ reason: reason ?? '(no reason given)',
303
+ stack: new Error('invalidated here').stack,
304
+ };
305
+ }
306
+ // A claim near a viewport that is waiting to blit makes that frame no
307
+ // longer a pure scroll — checked here, at claim time, because once the
308
+ // rects coalesce a change inside the viewport is indistinguishable from
309
+ // the scroll's own claim. (Unbounded claims need no check: FULL_DAMAGE
310
+ // fails the blit's damage gate by itself.)
311
+ // The region this claim actually covers — a node's paint reach, clipped
312
+ // to a blitting viewport above it (issue #398), or the bare rect a
313
+ // caller handed over. Null when the clip left nothing (the node draws
314
+ // where nothing can be seen, so it owes no pixels), and null on a frame
315
+ // that is already unbounded, which owes neither a rect nor the subtree
316
+ // walk that measures one — a blit cannot fire there either.
317
+ const bounds =
318
+ damage && damage !== NO_DAMAGE && this._damage !== FULL_DAMAGE
319
+ ? damage._claimBounds
320
+ ? damage._claimBounds()
321
+ : damage
322
+ : null;
323
+ const pendingScrolls = this._pendingScrolls;
324
+ if (pendingScrolls?.size && bounds && this._scrollClaim !== damage) {
325
+ const rect = bounds;
326
+ for (const sv of pendingScrolls) {
327
+ // An element blitting a region of its own drawing (issue #303) is
328
+ // waiting on that region, not on the whole node it lives in — and
329
+ // it is waiting on it *exactly* (issue #309). Its claim is the rect
330
+ // itself, and `_blitKeptDamage` recognises it as the rect itself, so
331
+ // a foreign claim that could be swallowed by it has to overlap it:
332
+ // the ring outside is beyond reach. A scroll container's claim is
333
+ // its viewport plus slop and is recognised to that tolerance, so a
334
+ // claim in that ring *can* merge into it without ever touching the
335
+ // viewport — and the wider zone is what keeps it out.
336
+ //
337
+ // The difference is what lets an element carve furniture out of the
338
+ // rect it blits — a minimap pinned to a corner, a strip it repaints
339
+ // itself — and keep the pan at blit cost while that furniture
340
+ // claims beside it.
341
+ const contents = sv._pendingBlitContents;
342
+ const waiting = contents
343
+ ? contents.rect
344
+ : sv.abs && insetRect(sv.abs, -(DAMAGE_SLOP * 2 + 1));
345
+ if (!waiting || rectsOverlap(rect, waiting)) {
346
+ // …unless this viewport is keeping a ledger of what changed
347
+ // inside it (issue #398): the region goes in the ledger and
348
+ // `_applyScrollBlits` repaints it after the blit, which is the
349
+ // same pixels on screen for a fraction of the drawing. The
350
+ // ledger says no when the frame stops paying, and then this
351
+ // falls through to the poison exactly as before.
352
+ if (sv._blitLedgerOpen() && sv._recordBlitClaim(rect)) {
353
+ continue;
354
+ }
355
+ // Poison rather than disarm (react-x11#295): a null here would
356
+ // let a second scrollTo in the same frame re-arm from a
357
+ // mid-frame origin, and the blit would then move pixels that
358
+ // were never repainted at that origin — a band displaced by the
359
+ // first scroll's delta. The node stays in pendingScrolls so the
360
+ // up-front clear in _applyScrollBlits resets the poison exactly
361
+ // like a real origin.
362
+ sv._pendingBlitFrom = BLIT_POISONED;
363
+ }
364
+ }
365
+ }
366
+ if (layoutChanged && !damage) this._damage = FULL_DAMAGE;
367
+ else if (!layoutChanged && !damage) this._damage = FULL_DAMAGE;
368
+ else if (!bounds) {
369
+ // A layout change that names no region: either NO_DAMAGE, from a
370
+ // caller with a finer claim already in flight, or a node whose reach
371
+ // a clipping ancestor left nothing of (issue #398). Unlike `!damage`
372
+ // neither is "something, somewhere", so neither costs a full repaint.
373
+ } else if (this._damage !== FULL_DAMAGE) {
374
+ // a node, or a bare rect for a caller that has a region rather than a
375
+ // node — a subtree that is about to be removed, say. Claims accumulate
376
+ // as a list of rects rather than one box around them all, so two changes
377
+ // at opposite corners of the window no longer repaint everything
378
+ // between them.
379
+ this._damage = addDamageRect(this._damage, bounds, this._damageRectCap());
380
+ }
381
+ this.needsPaint = true;
382
+ this._scheduleFrame();
383
+ }
384
+
385
+ /**
386
+ * The rects this frame will repaint, or null for the whole window.
387
+ *
388
+ * Clamped to the window: damage is recorded when a node invalidates, and
389
+ * the window may have been resized since. A region that no longer
390
+ * intersects the window means there is nothing to do, but the frame still
391
+ * has to clear the flag, so it degrades to a full repaint rather than
392
+ * painting nothing.
393
+ */
394
+ /**
395
+ * How many rects a frame's damage may hold before `addDamageRect` merges
396
+ * the closest pair. Four on X11 (`MAX_DAMAGE_RECTS`), where every pass
397
+ * costs the server a clip mask; a backend whose pass is a client-side
398
+ * clip and a culled walk says so on its window (`damageRectCap`) and
399
+ * keeps more of them — a clock, a graph and a status row ticking in one
400
+ * frame stay three small rects instead of the box around all three.
401
+ */
402
+ _damageRectCap() {
403
+ const cap = this.window?.damageRectCap;
404
+ return Number.isInteger(cap) && cap > 0 ? cap : MAX_DAMAGE_RECTS;
405
+ }
406
+
407
+ _takeDamage(width, height) {
408
+ const damage = this._damage;
409
+ this._damage = null;
410
+ // what the frame about to run settled on, for the tests and for
411
+ // REACT_X11_DEBUG_LAYOUT to report; null means it repainted everything.
412
+ // `_lastDamage` is the box around the rects, which is what a caller
413
+ // wanting one number for "where did this frame paint" means by it.
414
+ // `_lastReasons` is why: every reason invalidate() was given since the
415
+ // previous frame, for the frame log and the full-repaint warning.
416
+ this._lastDamage = null;
417
+ this._lastDamageRects = null;
418
+ const reasons = this._frameReasons;
419
+ if (reasons?.size) {
420
+ this._lastReasons = [...reasons];
421
+ reasons.clear();
422
+ } else {
423
+ this._lastReasons = EMPTY_REASONS;
424
+ }
425
+ if (damage === FULL_DAMAGE || !damage) return null;
426
+ // an empty list: every claim this frame made was answered on a layer of
427
+ // its own (`invalidate`, the presenter's `true`), and the bitmap paints
428
+ // nothing — which is not the same as nothing having been claimed
429
+ if (damage.length === 0) {
430
+ this._lastDamageRects = [];
431
+ this._lastDamage = { x: 0, y: 0, width: 0, height: 0 };
432
+ return [];
433
+ }
434
+ const rects = [];
435
+ for (const claimed of damage) {
436
+ const clamped = this._clampDamage(claimed, width, height);
437
+ // one claim covering the window makes the whole frame unbounded, so
438
+ // there is nothing to learn from the rest of the list
439
+ if (clamped === FULL_DAMAGE) return null;
440
+ if (clamped) rects.push(clamped);
441
+ }
442
+ if (!rects.length) return null;
443
+ this._lastDamageRects = damageToPaint(rects);
444
+ this._lastDamage = rectsBounds(this._lastDamageRects);
445
+ return this._lastDamageRects;
446
+ }
447
+
448
+ /**
449
+ * One claimed rect snapped to whole pixels inside the window: null when
450
+ * nothing of it is left, `FULL_DAMAGE` when it covers the window.
451
+ */
452
+ _clampDamage(damage, width, height) {
453
+ const x = Math.max(0, Math.floor(damage.x));
454
+ const y = Math.max(0, Math.floor(damage.y));
455
+ const right = Math.min(width, Math.ceil(damage.x + damage.width));
456
+ const bottom = Math.min(height, Math.ceil(damage.y + damage.height));
457
+ if (right <= x || bottom <= y) return null;
458
+ // covering the window is the same as not being bounded at all, and the
459
+ // full path is one fill instead of a clip plus a fill
460
+ if (x === 0 && y === 0 && right >= width && bottom >= height) {
461
+ return FULL_DAMAGE;
462
+ }
463
+ return { x, y, width: right - x, height: bottom - y };
464
+ }
465
+ }
@@ -0,0 +1,31 @@
1
+ // The kind tables. registry.js fills them when an element registers, and
2
+ // the node classes read them — that direction, rather than the nodes
3
+ // importing the registry, is what keeps the two acyclic.
4
+
5
+ /**
6
+ * Kinds that lay out with yoga and paint into the owning window —
7
+ * `paintOrder` filters on this, so a kind missing from it lays out and
8
+ * never paints. Mutable because `registerElement` (registry.js) adds to it;
9
+ * that direction, rather than src/nodes/ importing the registry, is what
10
+ * keeps the two files acyclic.
11
+ */
12
+ export const DRAWN_KINDS = new Set([
13
+ 'box',
14
+ 'text',
15
+ 'image',
16
+ 'canvas',
17
+ 'textinput',
18
+ 'textarea',
19
+ 'svg',
20
+ ]);
21
+
22
+ /** kind -> style names a registered element claims as its own semantics.
23
+ * Filled by registry.js; read by `Node.semanticNames`, so a registered
24
+ * element gets the exemption without subclassing the getter. */
25
+ export const CUSTOM_SEMANTIC_NAMES = new Map();
26
+
27
+ /** kind -> prop names a registered element claims the damage for itself.
28
+ * Filled by registry.js, read by `Node.selfDamagedProps` — the same
29
+ * arrangement as above, for the other declaration a scene-drawing element
30
+ * makes (issue #301). */
31
+ export const CUSTOM_SELF_DAMAGED = new Map();