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,334 @@
1
+ // Frames: asking the window's frame clock for one (through the frame pacer),
2
+ // and flush — layout, positions, the scroll blit, painting the damage.
3
+
4
+ import { addPendingFrame, clearPendingFrame } from '../../frames.js';
5
+ import { resolveFramePolicy } from '../../pacing.js';
6
+ import { paintCacheFor } from '../../paintcache.js';
7
+ import { hooks as traceHooks } from '../../trace-registry.js';
8
+ import { now } from '../animation.js';
9
+ import { FULL_DAMAGE, layoutDiff, addDamageRect } from '../damage.js';
10
+ import { BLIT_POISONED } from '../scrollblit.js';
11
+ import { debugPaint } from './debugpaint.js';
12
+
13
+ /** Frames, installed onto `WindowNode.prototype` by window.js. */
14
+ export class WindowFlush {
15
+ /**
16
+ * A frame, on the window's clock — after whatever wait the pacer asks
17
+ * for (src/pacing.js). Off by default, the pacer answers "now" for one
18
+ * property read; under an adaptive policy a claim that finds the window
19
+ * in debt for its last frames is held on a one-shot, and every claim
20
+ * until then folds into it. Nothing here changes what the frame paints:
21
+ * the damage accumulates on the node exactly as it does between two
22
+ * ticks of the clock.
23
+ */
24
+ _scheduleFrame() {
25
+ // Recorded before either gate, not inside them: the debt is "this
26
+ // window has damage", which a discrete event may pay off early (see
27
+ // frames.js). Tying it to whether a callback is outstanding would hide
28
+ // the second of two clicks a few milliseconds apart — the first one's
29
+ // frame is still scheduled, so this returns here, and the early flush
30
+ // would find nothing to paint. A held claim is a debt too: the early
31
+ // flush paints it, and `flush` stands the wait down.
32
+ addPendingFrame(this);
33
+ // A claim the frame raises on itself — an animation stepping, a
34
+ // container query settling, a promotion moving a node — is answered
35
+ // once the frame is over, so the pacer prices the frame that raised it
36
+ // and not the one before.
37
+ if (this._inFlush) {
38
+ this._claimAfterFlush = true;
39
+ return;
40
+ }
41
+ if (this._scheduled) return;
42
+ if (this._pacer.defer(() => this._requestFrame())) return;
43
+ this._requestFrame();
44
+ }
45
+
46
+ /** The callback on the window's clock. */
47
+ _requestFrame() {
48
+ if (this._scheduled || this.destroyed || !this.window) return;
49
+ this._scheduled = true;
50
+ const schedule =
51
+ typeof this.window.requestAnimationFrame === 'function'
52
+ ? (cb) => this.window.requestAnimationFrame(cb)
53
+ : (cb) => setImmediate(cb);
54
+ schedule(() => {
55
+ this._scheduled = false;
56
+ this.flush();
57
+ });
58
+ }
59
+
60
+ /**
61
+ * The policy this window paces its frames by: the environment, then the
62
+ * `frameRate` prop, then the root's `createRoot({ frameRate })`, then
63
+ * `'display'` (src/pacing.js). A bad value throws here — at mount or at
64
+ * the prop change — naming the value and the choices.
65
+ */
66
+ _syncFramePolicy() {
67
+ const policy = resolveFramePolicy(
68
+ this.props.frameRate,
69
+ this.app,
70
+ `<${this.kind} frameRate>`,
71
+ );
72
+ this._framePolicy = policy;
73
+ this._pacer.configure(policy);
74
+ }
75
+
76
+ /**
77
+ * The backend's half of a frame's cost, where it has one: the Cocoa
78
+ * present — the swapchain flip and its catch-up copy — runs after the
79
+ * flush returns, on the same thread, and is part of what the frame cost
80
+ * (src/cocoa/window.js reports it). An ntk window's present is one
81
+ * request, and reports nothing.
82
+ */
83
+ _notePresentCost(ms) {
84
+ this._pacer.charge(ms);
85
+ }
86
+
87
+ /**
88
+ * A frame: layout if owed, then the paint passes — or the presenter's
89
+ * frame — then the backend's word. Runs on the window's clock through
90
+ * `_scheduleFrame`, and early, synchronously, for a discrete input
91
+ * (frames.js). Every route lands here, so this is where a frame is
92
+ * priced: the pacer brackets the work, and what it cost is what the
93
+ * next claim is judged against (src/pacing.js).
94
+ */
95
+ flush() {
96
+ // Whatever this frame turns out to owe, it is this call's to pay — and
97
+ // a window that returns below because it is destroyed or unrealized
98
+ // owes nothing at all.
99
+ clearPendingFrame(this);
100
+ // …and a wait the pacer had armed for it has nothing left to wait for:
101
+ // whichever route got here first pays the same debt.
102
+ this._pacer.cancel();
103
+ if (this.destroyed || !this.yoga || !this.window) return;
104
+ const pacer = this._pacer;
105
+ pacer.began();
106
+ this._inFlush = true;
107
+ let painted = false;
108
+ try {
109
+ painted = this._flushFrame();
110
+ } finally {
111
+ this._inFlush = false;
112
+ pacer.ended(undefined, painted);
113
+ if (this._claimAfterFlush) {
114
+ this._claimAfterFlush = false;
115
+ if (!this.destroyed) this._scheduleFrame();
116
+ }
117
+ }
118
+ }
119
+
120
+ /** The frame itself. True when it painted or presented something. */
121
+ _flushFrame() {
122
+ // A frame is scheduled a tick before it is painted, and the connection
123
+ // can go in between: an app closing its own client, a server exit, a
124
+ // test closing the app it lent the root. Nothing unmounts the tree on
125
+ // that route, so the frame arrives with a live window node and a dead
126
+ // socket, and the first request it makes throws out of the frame clock
127
+ // where there is nothing waiting to catch it. There is no screen left to
128
+ // paint to, so this owes nothing either.
129
+ if (this.app?.X?._closing) return false;
130
+ // a transientFor whose owner was not realized yet at commit time. The
131
+ // frame after the mount is the first moment refs have attached, so the
132
+ // common "two <window>s in one tree" case resolves here rather than
133
+ // waiting for the app to re-render for some unrelated reason.
134
+ if (this._pendingTransientFor !== undefined) {
135
+ this._applyTransientFor(this._pendingTransientFor);
136
+ }
137
+ // A window that realized after a loop registered has one now
138
+ if (this._loopNodes.size && !this._loopWatch) this._watchLoops();
139
+ this._advanceAnimations(now());
140
+ if (this.needsLayout) {
141
+ // before `_refit` lays anything out: a pass clears yoga's record of
142
+ // which subtrees changed, and the floors are measured from that record
143
+ this._collectFloorStale();
144
+ this._refit();
145
+ }
146
+ const width = this.window.width ?? this._requestedSize?.width ?? 0;
147
+ const height = this.window.height ?? this._requestedSize?.height ?? 0;
148
+ let layoutMoved = false;
149
+ // captured before the branch clears it: whether *this* flush ran a
150
+ // layout pass is what decides whether an anchored popup needs a look,
151
+ // not the flag's post-pass value
152
+ const layoutRan = this.needsLayout;
153
+ if (this.needsLayout) {
154
+ // From here on a claim names where its content *landed*, not where it
155
+ // sat before the scroll — which is what decides whether a blit
156
+ // ledger's rect moves with the shift (issue #398).
157
+ this._laidOut = true;
158
+ this._resolveSizeQueries(width, height);
159
+ this._layoutStep(width, height);
160
+ // `@container` blocks are answered by the pass, not before it, and a
161
+ // changed answer is one more pass — before `absolutize`, so the layout
162
+ // diff below sees one arrangement against the last frame's
163
+ if (this._containerQueryNodes.size !== 0) {
164
+ this._settleContainerQueries(() => this._layoutStep(width, height));
165
+ }
166
+ this.abs = { x: 0, y: 0, width, height };
167
+ this._placed = true;
168
+ // the root's rect is written here, not through _assignAbs, so its
169
+ // cached hit reach is dropped here too (children bubble their own)
170
+ this._hitBoundsCache = null;
171
+ this._paintBoundsCache = null;
172
+ // A bounded frame watches the walk: whatever this pass actually moved
173
+ // claims its old and new rects through the sink, and the frame stays
174
+ // a few rects instead of the whole window. An unbounded frame skips
175
+ // the bookkeeping — it repaints everything anyway.
176
+ if (this._damage !== FULL_DAMAGE) {
177
+ const cap = this._damageRectCap();
178
+ layoutDiff.sink = (rect) => {
179
+ if (this._damage === FULL_DAMAGE) return;
180
+ layoutMoved = true;
181
+ this._damage = addDamageRect(this._damage, rect, cap);
182
+ };
183
+ }
184
+ try {
185
+ // `_absolutizeChildren`, not the loop it wraps: a `<window
186
+ // style={{overflow: 'scroll'}}>` is a scroll container like any box,
187
+ // and this is where its offset gets applied to the children
188
+ this._absolutizeChildren(0, 0);
189
+ } finally {
190
+ layoutDiff.sink = null;
191
+ }
192
+ // …and placed nodes against the arrangement that walk produced: where
193
+ // one goes depends on where its pane and its parent landed
194
+ if (this._placedNodes.size !== 0) this._placeNodes();
195
+ // A held scroll request the walk never reached goes, rather than
196
+ // landing on some later pass. A pane no pass has placed yet keeps it:
197
+ // its first placement is the pass it is waiting for.
198
+ if (this._heldScrolls?.size) {
199
+ for (const node of this._heldScrolls) {
200
+ if (node._childOrigin != null) node._scrollToTarget = null;
201
+ }
202
+ this._heldScrolls.clear();
203
+ }
204
+ this.needsLayout = false;
205
+ this.needsPaint = true;
206
+ // The other half of a contained reflow: the pre-mutation arrangement was
207
+ // claimed when the child list changed, and this is the arrangement that
208
+ // replaced it. Claimed after layout because an inserted child has no
209
+ // rect before it.
210
+ for (const node of this._reflowed) {
211
+ // …and the pre-mutation walk this frame reused goes with it
212
+ node._reflowBefore = null;
213
+ if (node.destroyed || this._damage === FULL_DAMAGE) continue;
214
+ // clipped to a blitting viewport above it, like every other claim
215
+ // this frame, and written to that viewport's ledger too (issue
216
+ // #398): the claim would otherwise coalesce into the scroll's own
217
+ // and be dropped with it, leaving the band the blit kept holding
218
+ // this node's pixels from before the reflow.
219
+ const after = node._claimBounds();
220
+ if (!after) continue;
221
+ const sv = node._blitViewport();
222
+ if (sv && !sv._recordBlitClaim(after)) {
223
+ sv._pendingBlitFrom = BLIT_POISONED;
224
+ }
225
+ this._damage = addDamageRect(
226
+ this._damage,
227
+ after,
228
+ this._damageRectCap(),
229
+ );
230
+ }
231
+ this._reflowed.clear();
232
+ } else if (this._reflowed.size) {
233
+ for (const node of this._reflowed) node._reflowBefore = null;
234
+ this._reflowed.clear();
235
+ }
236
+ // An animated placement asked for this frame and nothing laid out: its
237
+ // pass runs on its own, against the arrangement the last one left
238
+ if (!layoutRan && this._placementsDue && this._placeNodes()) {
239
+ this.needsPaint = true;
240
+ }
241
+ // A presenter compositing part of the tree on layers of its own — the
242
+ // surface presenter's promoted nodes (src/cocoa/promotion.js) — gets
243
+ // its word in here: after layout, so it sees where everything landed,
244
+ // and before the damage is taken, so a node it moves onto or off a
245
+ // layer claims the bitmap under it in this very frame. Feature-detected
246
+ // like `presentFrame`; an ntk window has no such half.
247
+ this.window.prepareFrame?.(this, layoutRan);
248
+ // any node this pass laid out may be what an open popup is anchored to
249
+ if (layoutRan) this._notifyAnchorChange();
250
+ // after layout (the claims above included), before the damage is taken:
251
+ // a frame that turns out to be a pure scroll blits the surviving band
252
+ // and narrows its claim to the exposed strip
253
+ this._applyScrollBlits(width, height, layoutMoved);
254
+ // …and the next commit's claims name the arrangement this frame leaves
255
+ // behind again, from before whatever scroll comes with them
256
+ this._laidOut = false;
257
+ if (!this.needsPaint) return false;
258
+ this.needsPaint = false;
259
+ const damage = this._takeDamage(width, height);
260
+ if (debugPaint === 'full' && !damage && width > 0 && height > 0) {
261
+ // Silent full-window repaints are the perf bug class this renderer
262
+ // actually has (see AGENTS.md); this is what surfaces them. The stack
263
+ // is the invalidate() call that made the frame unbounded, not this
264
+ // flush — flush is always the same place.
265
+ const cause = this._fullRepaintCause;
266
+ console.warn(
267
+ `react-x11: full-window repaint (${width}x${height}) ` +
268
+ `reasons=${this._lastReasons?.join('+') || '(none)'}` +
269
+ (cause ? `\n${cause.stack}` : ''),
270
+ );
271
+ }
272
+ this._fullRepaintCause = null;
273
+ // A retained presenter takes the frame from here: the model half above —
274
+ // animations, layout, absolutize, the scroll offsets — is shared, and
275
+ // what changes per backend is how a frame reaches the screen. The damage
276
+ // list was still taken (its bookkeeping is what keeps the two paths one
277
+ // code) and is simply not consumed; the presenter diffs at the layer.
278
+ if (typeof this.window.presentFrame === 'function') {
279
+ this.window.presentFrame(this, damage);
280
+ this.app._reactX11Startup?.painted();
281
+ return true;
282
+ }
283
+ if (typeof this.window.getContext !== 'function') return false; // headless mock
284
+ // ntk getContext creates a fresh context (with window-event
285
+ // subscriptions) on every call — cache one per window
286
+ const ctx = (this._ctx ??= this.window.getContext('2d'));
287
+ const frameHook = traceHooks.frame;
288
+ const started = frameHook ? performance.now() : 0;
289
+ if (debugPaint) this._flashTick = (this._flashTick ?? 0) + 1;
290
+ // One pass per damage rect, and a single pass over the whole window when
291
+ // there is no bound. Each pass clips to one rect rather than to all of
292
+ // them at once, which is what keeps ntk's server-side rectangular-clip
293
+ // fast path: a clip path holding several rects is not a rectangle, and
294
+ // falls back to rasterizing a full-surface mask.
295
+ this._paintCache ??= paintCacheFor(this.app);
296
+ this._paintCache?.beginFrame();
297
+ for (const rect of damage ?? [null]) {
298
+ this._paintRegion(ctx, rect, width, height);
299
+ }
300
+ // after every region: an entry drawn in one damage rect must not be
301
+ // evicted before the next rect of the same frame asks for it
302
+ this._paintCache?.endFrame();
303
+ // The swapchain seam: a backend presenting from double buffers has to
304
+ // know exactly which pixels each flush touched — several flushes can
305
+ // land between two presents, so reading only the last frame's rects
306
+ // would leave the flipped-in back buffer stale where an earlier flush
307
+ // painted. Feature-detected like presentFrame; null means everything.
308
+ this.window.noteFrameDamage?.(damage ?? null);
309
+ if (frameHook) {
310
+ frameHook({
311
+ root: this,
312
+ rects: damage,
313
+ reasons: this._lastReasons,
314
+ start: started,
315
+ end: performance.now(),
316
+ // ntk's `frameLatency`: how long the previous frame took to be
317
+ // answered. On the vertical-blank clock that is time-to-display and
318
+ // reads about a refresh period; on the fence clock it is the server
319
+ // round trip that drained the frame's requests. Client work and
320
+ // server work separate cleanly in a trace only when both are in it —
321
+ // a slow virtualized GPU shows up here, not in `end`.
322
+ landed: this.window.frameLatency,
323
+ // how long the pacer held this frame's claim, ms; 0 when it did not
324
+ waited: this._pacer.pendingWait,
325
+ });
326
+ }
327
+ // A frame that actually painted, which is the moment the app is up
328
+ // (src/startup.js). One property read once the sequence is over — the
329
+ // session clears itself off the app — which is the same bargain the
330
+ // trace hook above makes with the frame loop.
331
+ this.app._reactX11Startup?.painted();
332
+ return true;
333
+ }
334
+ }