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.
- package/README.md +278 -129
- package/package.json +12 -4
- package/src/Reconciler.js +19 -31
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/appcontext.js +59 -30
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +303 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +24 -5
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +17 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/embedding.js +31 -0
- package/src/errors.js +46 -0
- package/src/events.js +78 -18
- package/src/foreignnodes.js +59 -5
- package/src/frames.js +2 -2
- package/src/glnodes.js +172 -41
- package/src/gloverlay.js +383 -0
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -1
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +34 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +16 -3
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +369 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +954 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +26 -14
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
|
@@ -0,0 +1,369 @@
|
|
|
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
|
+
* Panes for the children of every `<glarea>` here, where this frame's
|
|
89
|
+
* layout put them (src/gloverlay.js). True when one was made, resized or
|
|
90
|
+
* dropped, which the frame then owes a paint for.
|
|
91
|
+
*/
|
|
92
|
+
_syncOverlays() {
|
|
93
|
+
let changed = false;
|
|
94
|
+
for (const area of this._overlaid) {
|
|
95
|
+
if (area.destroyed || area.root !== this) {
|
|
96
|
+
this._overlaid.delete(area);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (area._syncOverlay()) changed = true;
|
|
100
|
+
}
|
|
101
|
+
return changed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** …and their paint, with the frame's own damage: a claim inside a
|
|
105
|
+
* `<glarea>`'s children is in that list like any other. */
|
|
106
|
+
_paintOverlays(damage) {
|
|
107
|
+
for (const area of this._overlaid) area._paintOverlay(damage);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A frame: layout if owed, then the paint passes — or the presenter's
|
|
112
|
+
* frame — then the backend's word. Runs on the window's clock through
|
|
113
|
+
* `_scheduleFrame`, and early, synchronously, for a discrete input
|
|
114
|
+
* (frames.js). Every route lands here, so this is where a frame is
|
|
115
|
+
* priced: the pacer brackets the work, and what it cost is what the
|
|
116
|
+
* next claim is judged against (src/pacing.js).
|
|
117
|
+
*/
|
|
118
|
+
flush() {
|
|
119
|
+
// Whatever this frame turns out to owe, it is this call's to pay — and
|
|
120
|
+
// a window that returns below because it is destroyed or unrealized
|
|
121
|
+
// owes nothing at all.
|
|
122
|
+
clearPendingFrame(this);
|
|
123
|
+
// …and a wait the pacer had armed for it has nothing left to wait for:
|
|
124
|
+
// whichever route got here first pays the same debt.
|
|
125
|
+
this._pacer.cancel();
|
|
126
|
+
if (this.destroyed || !this.yoga || !this.window) return;
|
|
127
|
+
const pacer = this._pacer;
|
|
128
|
+
pacer.began();
|
|
129
|
+
this._inFlush = true;
|
|
130
|
+
let painted = false;
|
|
131
|
+
try {
|
|
132
|
+
painted = this._flushFrame();
|
|
133
|
+
} finally {
|
|
134
|
+
this._inFlush = false;
|
|
135
|
+
pacer.ended(undefined, painted);
|
|
136
|
+
if (this._claimAfterFlush) {
|
|
137
|
+
this._claimAfterFlush = false;
|
|
138
|
+
if (!this.destroyed) this._scheduleFrame();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The frame itself. True when it painted or presented something. */
|
|
144
|
+
_flushFrame() {
|
|
145
|
+
// A frame is scheduled a tick before it is painted, and the connection
|
|
146
|
+
// can go in between: an app closing its own client, a server exit, a
|
|
147
|
+
// test closing the app it lent the root. Nothing unmounts the tree on
|
|
148
|
+
// that route, so the frame arrives with a live window node and a dead
|
|
149
|
+
// socket, and the first request it makes throws out of the frame clock
|
|
150
|
+
// where there is nothing waiting to catch it. There is no screen left to
|
|
151
|
+
// paint to, so this owes nothing either.
|
|
152
|
+
if (this.app?.X?._closing) return false;
|
|
153
|
+
// a transientFor whose owner was not realized yet at commit time. The
|
|
154
|
+
// frame after the mount is the first moment refs have attached, so the
|
|
155
|
+
// common "two <window>s in one tree" case resolves here rather than
|
|
156
|
+
// waiting for the app to re-render for some unrelated reason.
|
|
157
|
+
if (this._pendingTransientFor !== undefined) {
|
|
158
|
+
this._applyTransientFor(this._pendingTransientFor);
|
|
159
|
+
}
|
|
160
|
+
// A window that realized after a loop registered has one now
|
|
161
|
+
if (this._loopNodes.size && !this._loopWatch) this._watchLoops();
|
|
162
|
+
this._advanceAnimations(now());
|
|
163
|
+
if (this.needsLayout) {
|
|
164
|
+
// before `_refit` lays anything out: a pass clears yoga's record of
|
|
165
|
+
// which subtrees changed, and the floors are measured from that record
|
|
166
|
+
this._collectFloorStale();
|
|
167
|
+
this._refit();
|
|
168
|
+
}
|
|
169
|
+
const width = this.window.width ?? this._requestedSize?.width ?? 0;
|
|
170
|
+
const height = this.window.height ?? this._requestedSize?.height ?? 0;
|
|
171
|
+
let layoutMoved = false;
|
|
172
|
+
// captured before the branch clears it: whether *this* flush ran a
|
|
173
|
+
// layout pass is what decides whether an anchored popup needs a look,
|
|
174
|
+
// not the flag's post-pass value
|
|
175
|
+
const layoutRan = this.needsLayout;
|
|
176
|
+
if (this.needsLayout) {
|
|
177
|
+
// From here on a claim names where its content *landed*, not where it
|
|
178
|
+
// sat before the scroll — which is what decides whether a blit
|
|
179
|
+
// ledger's rect moves with the shift (issue #398).
|
|
180
|
+
this._laidOut = true;
|
|
181
|
+
this._resolveSizeQueries(width, height);
|
|
182
|
+
this._layoutStep(width, height);
|
|
183
|
+
// `@container` blocks are answered by the pass, not before it, and a
|
|
184
|
+
// changed answer is one more pass — before `absolutize`, so the layout
|
|
185
|
+
// diff below sees one arrangement against the last frame's
|
|
186
|
+
if (this._containerQueryNodes.size !== 0) {
|
|
187
|
+
this._settleContainerQueries(() => this._layoutStep(width, height));
|
|
188
|
+
}
|
|
189
|
+
this.abs = { x: 0, y: 0, width, height };
|
|
190
|
+
this._placed = true;
|
|
191
|
+
// the root's rect is written here, not through _assignAbs, so its
|
|
192
|
+
// cached hit reach is dropped here too (children bubble their own)
|
|
193
|
+
this._hitBoundsCache = null;
|
|
194
|
+
this._paintBoundsCache = null;
|
|
195
|
+
// A bounded frame watches the walk: whatever this pass actually moved
|
|
196
|
+
// claims its old and new rects through the sink, and the frame stays
|
|
197
|
+
// a few rects instead of the whole window. An unbounded frame skips
|
|
198
|
+
// the bookkeeping — it repaints everything anyway.
|
|
199
|
+
if (this._damage !== FULL_DAMAGE) {
|
|
200
|
+
const cap = this._damageRectCap();
|
|
201
|
+
layoutDiff.sink = (rect) => {
|
|
202
|
+
if (this._damage === FULL_DAMAGE) return;
|
|
203
|
+
layoutMoved = true;
|
|
204
|
+
this._damage = addDamageRect(this._damage, rect, cap);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
// `_absolutizeChildren`, not the loop it wraps: a `<window
|
|
209
|
+
// style={{overflow: 'scroll'}}>` is a scroll container like any box,
|
|
210
|
+
// and this is where its offset gets applied to the children
|
|
211
|
+
this._absolutizeChildren(0, 0);
|
|
212
|
+
} finally {
|
|
213
|
+
layoutDiff.sink = null;
|
|
214
|
+
}
|
|
215
|
+
// …and placed nodes against the arrangement that walk produced: where
|
|
216
|
+
// one goes depends on where its pane and its parent landed
|
|
217
|
+
if (this._placedNodes.size !== 0) this._placeNodes();
|
|
218
|
+
// A held scroll request the walk never reached goes, rather than
|
|
219
|
+
// landing on some later pass. A pane no pass has placed yet keeps it:
|
|
220
|
+
// its first placement is the pass it is waiting for.
|
|
221
|
+
if (this._heldScrolls?.size) {
|
|
222
|
+
for (const node of this._heldScrolls) {
|
|
223
|
+
if (node._childOrigin != null) node._scrollToTarget = null;
|
|
224
|
+
}
|
|
225
|
+
this._heldScrolls.clear();
|
|
226
|
+
}
|
|
227
|
+
this.needsLayout = false;
|
|
228
|
+
this.needsPaint = true;
|
|
229
|
+
// The other half of a contained reflow: the pre-mutation arrangement was
|
|
230
|
+
// claimed when the child list changed, and this is the arrangement that
|
|
231
|
+
// replaced it. Claimed after layout because an inserted child has no
|
|
232
|
+
// rect before it.
|
|
233
|
+
for (const node of this._reflowed) {
|
|
234
|
+
// …and the pre-mutation walk this frame reused goes with it
|
|
235
|
+
node._reflowBefore = null;
|
|
236
|
+
if (node.destroyed || this._damage === FULL_DAMAGE) continue;
|
|
237
|
+
// clipped to a blitting viewport above it, like every other claim
|
|
238
|
+
// this frame, and written to that viewport's ledger too (issue
|
|
239
|
+
// #398): the claim would otherwise coalesce into the scroll's own
|
|
240
|
+
// and be dropped with it, leaving the band the blit kept holding
|
|
241
|
+
// this node's pixels from before the reflow.
|
|
242
|
+
const after = node._claimBounds();
|
|
243
|
+
if (!after) continue;
|
|
244
|
+
const sv = node._blitViewport();
|
|
245
|
+
if (sv && !sv._recordBlitClaim(after)) {
|
|
246
|
+
sv._pendingBlitFrom = BLIT_POISONED;
|
|
247
|
+
}
|
|
248
|
+
this._damage = addDamageRect(
|
|
249
|
+
this._damage,
|
|
250
|
+
after,
|
|
251
|
+
this._damageRectCap(),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
this._reflowed.clear();
|
|
255
|
+
} else if (this._reflowed.size) {
|
|
256
|
+
for (const node of this._reflowed) node._reflowBefore = null;
|
|
257
|
+
this._reflowed.clear();
|
|
258
|
+
}
|
|
259
|
+
// An animated placement asked for this frame and nothing laid out: its
|
|
260
|
+
// pass runs on its own, against the arrangement the last one left
|
|
261
|
+
if (!layoutRan && this._placementsDue && this._placeNodes()) {
|
|
262
|
+
this.needsPaint = true;
|
|
263
|
+
}
|
|
264
|
+
// A presenter compositing part of the tree on layers of its own — the
|
|
265
|
+
// surface presenter's promoted nodes (src/cocoa/promotion.js) — gets
|
|
266
|
+
// its word in here: after layout, so it sees where everything landed,
|
|
267
|
+
// and before the damage is taken, so a node it moves onto or off a
|
|
268
|
+
// layer claims the bitmap under it in this very frame. Feature-detected
|
|
269
|
+
// like `presentFrame`; an ntk window has no such half.
|
|
270
|
+
this.window.prepareFrame?.(this, layoutRan);
|
|
271
|
+
// any node this pass laid out may be what an open popup is anchored to
|
|
272
|
+
if (layoutRan) this._notifyAnchorChange();
|
|
273
|
+
// after layout (the claims above included), before the damage is taken:
|
|
274
|
+
// a frame that turns out to be a pure scroll blits the surviving band
|
|
275
|
+
// and narrows its claim to the exposed strip
|
|
276
|
+
this._applyScrollBlits(width, height, layoutMoved);
|
|
277
|
+
// …and the next commit's claims name the arrangement this frame leaves
|
|
278
|
+
// behind again, from before whatever scroll comes with them
|
|
279
|
+
this._laidOut = false;
|
|
280
|
+
// The children of a `<glarea>` get panes where they are now: after
|
|
281
|
+
// layout and the scroll's shift, before the damage is taken, so a pane
|
|
282
|
+
// made or resized in this frame is painted in it, whole
|
|
283
|
+
if (this._overlaid.size !== 0 && this._syncOverlays()) {
|
|
284
|
+
this.needsPaint = true;
|
|
285
|
+
}
|
|
286
|
+
if (!this.needsPaint) return false;
|
|
287
|
+
this.needsPaint = false;
|
|
288
|
+
const damage = this._takeDamage(width, height);
|
|
289
|
+
if (debugPaint === 'full' && !damage && width > 0 && height > 0) {
|
|
290
|
+
// Silent full-window repaints are the perf bug class this renderer
|
|
291
|
+
// actually has (see AGENTS.md); this is what surfaces them. The stack
|
|
292
|
+
// is the invalidate() call that made the frame unbounded, not this
|
|
293
|
+
// flush — flush is always the same place.
|
|
294
|
+
const cause = this._fullRepaintCause;
|
|
295
|
+
console.warn(
|
|
296
|
+
`react-x11: full-window repaint (${width}x${height}) ` +
|
|
297
|
+
`reasons=${this._lastReasons?.join('+') || '(none)'}` +
|
|
298
|
+
(cause ? `\n${cause.stack}` : ''),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
this._fullRepaintCause = null;
|
|
302
|
+
// A retained presenter takes the frame from here: the model half above —
|
|
303
|
+
// animations, layout, absolutize, the scroll offsets — is shared, and
|
|
304
|
+
// what changes per backend is how a frame reaches the screen. The damage
|
|
305
|
+
// list was still taken (its bookkeeping is what keeps the two paths one
|
|
306
|
+
// code) and is simply not consumed; the presenter diffs at the layer.
|
|
307
|
+
if (typeof this.window.presentFrame === 'function') {
|
|
308
|
+
// the panes are no presenter's: they paint from the damage either way
|
|
309
|
+
if (this._overlaid.size !== 0) this._paintOverlays(damage);
|
|
310
|
+
this.window.presentFrame(this, damage);
|
|
311
|
+
this.app._reactX11Startup?.painted();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (typeof this.window.getContext !== 'function') return false; // headless mock
|
|
315
|
+
// ntk getContext creates a fresh context (with window-event
|
|
316
|
+
// subscriptions) on every call — cache one per window
|
|
317
|
+
const ctx = (this._ctx ??= this.window.getContext('2d'));
|
|
318
|
+
const frameHook = traceHooks.frame;
|
|
319
|
+
const started = frameHook ? performance.now() : 0;
|
|
320
|
+
if (debugPaint) this._flashTick = (this._flashTick ?? 0) + 1;
|
|
321
|
+
// One pass per damage rect, and a single pass over the whole window when
|
|
322
|
+
// there is no bound. Each pass clips to one rect rather than to all of
|
|
323
|
+
// them at once, which is what keeps ntk's server-side rectangular-clip
|
|
324
|
+
// fast path: a clip path holding several rects is not a rectangle, and
|
|
325
|
+
// falls back to rasterizing a full-surface mask.
|
|
326
|
+
this._paintCache ??= paintCacheFor(this.app);
|
|
327
|
+
this._paintCache?.beginFrame();
|
|
328
|
+
for (const rect of damage ?? [null]) {
|
|
329
|
+
this._paintRegion(ctx, rect, width, height);
|
|
330
|
+
}
|
|
331
|
+
// …and the panes over the surfaces, with the same damage: a claim from
|
|
332
|
+
// a `<glarea>`'s children is theirs to repaint — the window's pass under
|
|
333
|
+
// the surface is one nobody sees. Inside the cache's frame, like a pass.
|
|
334
|
+
if (this._overlaid.size !== 0) this._paintOverlays(damage);
|
|
335
|
+
// after every region: an entry drawn in one damage rect must not be
|
|
336
|
+
// evicted before the next rect of the same frame asks for it
|
|
337
|
+
this._paintCache?.endFrame();
|
|
338
|
+
// The swapchain seam: a backend presenting from double buffers has to
|
|
339
|
+
// know exactly which pixels each flush touched — several flushes can
|
|
340
|
+
// land between two presents, so reading only the last frame's rects
|
|
341
|
+
// would leave the flipped-in back buffer stale where an earlier flush
|
|
342
|
+
// painted. Feature-detected like presentFrame; null means everything.
|
|
343
|
+
this.window.noteFrameDamage?.(damage ?? null);
|
|
344
|
+
if (frameHook) {
|
|
345
|
+
frameHook({
|
|
346
|
+
root: this,
|
|
347
|
+
rects: damage,
|
|
348
|
+
reasons: this._lastReasons,
|
|
349
|
+
start: started,
|
|
350
|
+
end: performance.now(),
|
|
351
|
+
// ntk's `frameLatency`: how long the previous frame took to be
|
|
352
|
+
// answered. On the vertical-blank clock that is time-to-display and
|
|
353
|
+
// reads about a refresh period; on the fence clock it is the server
|
|
354
|
+
// round trip that drained the frame's requests. Client work and
|
|
355
|
+
// server work separate cleanly in a trace only when both are in it —
|
|
356
|
+
// a slow virtualized GPU shows up here, not in `end`.
|
|
357
|
+
landed: this.window.frameLatency,
|
|
358
|
+
// how long the pacer held this frame's claim, ms; 0 when it did not
|
|
359
|
+
waited: this._pacer.pendingWait,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// A frame that actually painted, which is the moment the app is up
|
|
363
|
+
// (src/startup.js). One property read once the sequence is over — the
|
|
364
|
+
// session clears itself off the app — which is the same bargain the
|
|
365
|
+
// trace hook above makes with the frame loop.
|
|
366
|
+
this.app._reactX11Startup?.painted();
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
}
|