react-x11 2.40.0 → 2.41.1
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/package.json +4 -4
- package/src/cocoa/app.js +19 -8
- package/src/cocoa/keymap.js +69 -12
- package/src/cocoa/panehost.js +92 -19
- package/src/cocoa/paneplayer.js +101 -24
- package/src/cocoa/panewindow.js +325 -44
- package/src/cocoa/window.js +9 -1
- package/src/events.js +12 -0
- package/src/frame/index.js +63 -17
- package/src/nodes/textarea.js +85 -15
- package/src/nodes/textinput.js +14 -3
- package/src/nodes/textkeys.js +322 -0
- package/src/offscreen.js +8 -7
- package/src/types/events.d.ts +6 -2
- package/src/win32/fonts.js +635 -47
- package/src/win32/panewindow.js +25 -2
package/src/cocoa/panewindow.js
CHANGED
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
// (src/cocoa/panehost.js), because nothing in a pane process runs AppKit.
|
|
22
22
|
import { BackendContext2D } from '../backend/context2d.js';
|
|
23
23
|
|
|
24
|
+
// How long a pane counts on the host's `pane-shown` for a present before it
|
|
25
|
+
// stops waiting for one (`_answerOverdue`). A host that is only busy answers
|
|
26
|
+
// well inside it — in the browser example on a Mac, a present was looked up
|
|
27
|
+
// under a millisecond after it went at the median, and 180ms after at
|
|
28
|
+
// worst, the host laying out a page at each width of a drag on its first
|
|
29
|
+
// run — and one that never answers, a host stalled or gone, must not stop
|
|
30
|
+
// the pane drawing.
|
|
31
|
+
const ANSWER_TIMEOUT_MS = 250;
|
|
32
|
+
|
|
24
33
|
let nextPaneId = 1;
|
|
25
34
|
|
|
26
35
|
export class CocoaPaneWindow {
|
|
@@ -57,7 +66,23 @@ export class CocoaPaneWindow {
|
|
|
57
66
|
this._dirty = false;
|
|
58
67
|
this._flushDamage = 'full';
|
|
59
68
|
this._seq = 0;
|
|
69
|
+
// a size from the host not yet presented (`pane-sized`)
|
|
70
|
+
this._owesSized = false;
|
|
60
71
|
this._presentedAt = -Infinity;
|
|
72
|
+
// the window's own frame clock (`CocoaApp._frameDue`), at the app's
|
|
73
|
+
// interval: a pane has no display of its own to pace by
|
|
74
|
+
this._rafLast = 0;
|
|
75
|
+
// The newest present the host has looked up (`pane-shown`), and the
|
|
76
|
+
// buffers of retired rings still named by a present it has not: freed
|
|
77
|
+
// as it does (`_releaseRing`).
|
|
78
|
+
this._shownSeq = 0;
|
|
79
|
+
this._shownAt = -Infinity;
|
|
80
|
+
// what it had looked up before its latest answers, whose buffer the
|
|
81
|
+
// screen may still show (`_takeBack`)
|
|
82
|
+
this._shownBefore = 0;
|
|
83
|
+
this._pollTimer = null;
|
|
84
|
+
this._kickQueued = false;
|
|
85
|
+
this._retired = [];
|
|
61
86
|
this._reactX11Node = null;
|
|
62
87
|
// Where the pane's corner is on the screen, in device pixels, as the
|
|
63
88
|
// host last said (`setPaneSize`): what a popup the pane anchors to one
|
|
@@ -89,6 +114,10 @@ export class CocoaPaneWindow {
|
|
|
89
114
|
const moved =
|
|
90
115
|
screen != null && (screen.x !== this.x || screen.y !== this.y);
|
|
91
116
|
const resized = w !== this.width || h !== this.height;
|
|
117
|
+
// The host sends the next rect when this one is painted (`pane-sized`,
|
|
118
|
+
// src/frame/index.js): one that changes no size is painted already.
|
|
119
|
+
if (resized) this._owesSized = true;
|
|
120
|
+
else this._post({ type: 'pane-sized' });
|
|
92
121
|
if (!moved && !resized) return;
|
|
93
122
|
if (moved) {
|
|
94
123
|
this.x = screen.x;
|
|
@@ -143,8 +172,10 @@ export class CocoaPaneWindow {
|
|
|
143
172
|
this.app._paneSend?.(msg);
|
|
144
173
|
}
|
|
145
174
|
|
|
175
|
+
/** On this window's clock, which the clock asks before it runs a frame
|
|
176
|
+
* (`frameHeld`). */
|
|
146
177
|
requestAnimationFrame(cb) {
|
|
147
|
-
return this.app._requestFrame(cb);
|
|
178
|
+
return this.app._requestFrame(cb, this);
|
|
148
179
|
}
|
|
149
180
|
|
|
150
181
|
/** A `<glarea>`'s frame, after this pane's own (`CocoaWindow`'s). */
|
|
@@ -152,34 +183,140 @@ export class CocoaPaneWindow {
|
|
|
152
183
|
return this.app._requestFrame(cb, null, true);
|
|
153
184
|
}
|
|
154
185
|
|
|
186
|
+
/** The clock's period (`CocoaApp._frameDue`): the app's, a pane having no
|
|
187
|
+
* display of its own. */
|
|
188
|
+
get _frameInterval() {
|
|
189
|
+
return this.app.frameIntervalFor(null);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** What is on glass is the host's to know; a pane draws what it is
|
|
193
|
+
* asked to (`CocoaApp._tickFrames`). */
|
|
194
|
+
_visible() {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
/**
|
|
156
199
|
* Whether the host may still be showing the frame before the last one —
|
|
157
200
|
* the gate `flushPendingFrames` (src/frames.js) is written around: a
|
|
158
201
|
* discrete input paints on the spot only when the last frame has landed,
|
|
159
202
|
* which is what folds a burst into one paced frame instead of a frame per
|
|
160
|
-
* event.
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
203
|
+
* event. Without a gate here every message the host queued while the
|
|
204
|
+
* pane was busy was answered with a full frame of its own: a forty-tick
|
|
205
|
+
* resize of a pane whose frame costs 300ms stepped through forty sizes
|
|
206
|
+
* for twelve seconds after the drag had ended, each one the previous
|
|
207
|
+
* surface stretched to the layer.
|
|
208
|
+
*
|
|
209
|
+
* On X11 the server says when a present was shown. Here the host says
|
|
210
|
+
* when it has looked one up (`pane-shown`), which is half of it: the host
|
|
211
|
+
* commits on its own thread and the screen shows the frame at the
|
|
212
|
+
* refresh after. So a present is in flight until it is answered, and for
|
|
213
|
+
* a frame interval after it went: measured in the browser example on a
|
|
214
|
+
* 120Hz panel, the answer alone answered an input 4ms sooner, and gave
|
|
215
|
+
* input faster than the display a frame each — 245 a second at 250
|
|
216
|
+
* inputs a second, 355 at 1000 — where the interval keeps it at 118, and
|
|
217
|
+
* the frames that outran the WindowServer drew into buffers it still
|
|
218
|
+
* read. Nothing the pane can ask says when a frame is on glass: a buffer
|
|
219
|
+
* reads in use from the host's lookup, not from the refresh. And no frame
|
|
220
|
+
* is drawn while one is held (`frameHeld`), whatever asks.
|
|
168
221
|
*/
|
|
169
222
|
frameInFlight() {
|
|
223
|
+
if (this.frameHeld()) return true;
|
|
224
|
+
if (this._seq > this._shownSeq && !this._answerOverdue()) return true;
|
|
170
225
|
return (
|
|
171
226
|
performance.now() - this._presentedAt < this.app.frameIntervalFor(null)
|
|
172
227
|
);
|
|
173
228
|
}
|
|
174
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Whether this pane's next frame has no buffer to go into, which holds it
|
|
232
|
+
* on the clock (`CocoaApp._tickFrames`): every buffer but the one it last
|
|
233
|
+
* presented is named by a present the host is showing or has yet to look
|
|
234
|
+
* up, or the host is done with one and the WindowServer is still reading
|
|
235
|
+
* it. A frame drawn into one of those anyway was a frame half drawn on
|
|
236
|
+
* glass — the host showed the buffer the pane was drawing into, or looked
|
|
237
|
+
* it up after the pane had started, and showed that.
|
|
238
|
+
*
|
|
239
|
+
* The second is the host coming back from a long frame of its own. Its
|
|
240
|
+
* answers say it has looked the queued presents up, not that they are on
|
|
241
|
+
* glass: that is its UI thread's commit and the refresh after, and until
|
|
242
|
+
* then the buffer it showed all along is still what the screen shows —
|
|
243
|
+
* and which buffer that is, the answers cannot say, since a host that
|
|
244
|
+
* stalled between a lookup and its commit left the one before on glass.
|
|
245
|
+
* The WindowServer can: a buffer is taken once it has let go of one, which
|
|
246
|
+
* is asked every millisecond (`_pollHeld`), and for a frame interval after
|
|
247
|
+
* the host's answer at most, since it reads a buffer it no longer shows
|
|
248
|
+
* for as long as it likes and a pane must not wait on that.
|
|
249
|
+
*
|
|
250
|
+
* A new size is never held: it draws into a new ring, which nothing
|
|
251
|
+
* names. Nor is a host that stopped answering (`_answerOverdue`).
|
|
252
|
+
*/
|
|
253
|
+
frameHeld() {
|
|
254
|
+
if (!this._ring || this._surface) return false;
|
|
255
|
+
if (
|
|
256
|
+
this._surfaceSize?.width !== this.width ||
|
|
257
|
+
this._surfaceSize?.height !== this.height
|
|
258
|
+
) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
let done = false;
|
|
262
|
+
for (let i = 0; i < this._ring.length; i += 1) {
|
|
263
|
+
const s = this._ring[i];
|
|
264
|
+
if (i === this._shownIndex || !this._done(s)) continue;
|
|
265
|
+
if (!this._held(s)) return false;
|
|
266
|
+
done = true;
|
|
267
|
+
}
|
|
268
|
+
if (!done) return !this._answerOverdue();
|
|
269
|
+
const wait =
|
|
270
|
+
this._shownAt + this.app.frameIntervalFor(null) - performance.now();
|
|
271
|
+
if (!(wait > 0)) return false;
|
|
272
|
+
this._pollHeld();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Ask again in a millisecond whether the WindowServer has let go of a
|
|
278
|
+
* buffer (`frameHeld`): it says so by no event, and the pump's next tick
|
|
279
|
+
* is up to 8ms off — a frame held that long for a buffer let go of a
|
|
280
|
+
* millisecond later was a frame late.
|
|
281
|
+
*/
|
|
282
|
+
_pollHeld() {
|
|
283
|
+
if (this._pollTimer) return;
|
|
284
|
+
this._pollTimer = setTimeout(() => {
|
|
285
|
+
this._pollTimer = null;
|
|
286
|
+
const app = this.app;
|
|
287
|
+
if (!this.destroyed && app._rafQueue.some((e) => e.wnd === this)) {
|
|
288
|
+
app._tickFrames();
|
|
289
|
+
app._presentAll();
|
|
290
|
+
}
|
|
291
|
+
}, 1);
|
|
292
|
+
this._pollTimer.unref?.();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Whether the host has looked up a present after the last one that named
|
|
296
|
+
* `buffer`, or none ever did: nothing will show it again. */
|
|
297
|
+
_done(buffer) {
|
|
298
|
+
return !buffer.presentedSeq || buffer.presentedSeq < this._shownSeq;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A present the host has not answered for longer than it would if it
|
|
302
|
+
* were answering at all (`ANSWER_TIMEOUT_MS`). */
|
|
303
|
+
_answerOverdue() {
|
|
304
|
+
return (
|
|
305
|
+
this._seq > this._shownSeq &&
|
|
306
|
+
performance.now() - this._presentedAt >= ANSWER_TIMEOUT_MS
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
175
310
|
// Three buffers, not two. A pane is cross-process: the host keeps
|
|
176
311
|
// scanning the last buffer it was handed until it processes the next
|
|
177
|
-
// present message
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
312
|
+
// present message. With two buffers the next paint (and the catch-up
|
|
313
|
+
// copy) lands in the very buffer the host is still displaying — every
|
|
314
|
+
// present tears it, the flash. With three, one present may be on its way
|
|
315
|
+
// while the next frame draws: the buffer the host shows, the one the
|
|
316
|
+
// present on its way names, and the one the host has let go of. A host
|
|
317
|
+
// two presents behind leaves none, and the frame waits for its answer
|
|
318
|
+
// (`frameHeld`, `_takeBack`). Same-process windows need only two because
|
|
319
|
+
// Core Animation latches the front buffer.
|
|
183
320
|
static RING = 3;
|
|
184
321
|
|
|
185
322
|
/**
|
|
@@ -188,11 +325,26 @@ export class CocoaPaneWindow {
|
|
|
188
325
|
* drag resizes the pane a tick at a time, and each tick retires a ring
|
|
189
326
|
* that the finalizer would have held until a collection happened to run.
|
|
190
327
|
* The host keeps its own reference to whichever buffer its layer shows,
|
|
191
|
-
* so the frame on glass survives the free
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
328
|
+
* so the frame on glass survives the free.
|
|
329
|
+
*
|
|
330
|
+
* All but a buffer a present names that the host has not looked up yet.
|
|
331
|
+
* The host finds a buffer by its IOSurface id, and the system hands a
|
|
332
|
+
* freed id to the next surface anyone makes — the three this ring's
|
|
333
|
+
* successor is about to make, the ring of another pane resized in the
|
|
334
|
+
* same layout, the host's own window. A present still in the channel
|
|
335
|
+
* named the id, not the surface, so the host scanned out whatever had it
|
|
336
|
+
* by the time it looked: a frame of this pane's next ring, cleared or
|
|
337
|
+
* half drawn, or another tab's page, for a frame at every few steps of a
|
|
338
|
+
* drag. So that buffer is kept (`_retired`) until the host says it has
|
|
339
|
+
* looked it up (`pane-shown`, `_shown`), and only then freed: its id
|
|
340
|
+
* stays its own until nothing can ask for it. That is the buffers of
|
|
341
|
+
* the presents in flight — one, as a rule, since a present goes a frame
|
|
342
|
+
* at a time and the host answers on its next turn — and never more than
|
|
343
|
+
* a ring's worth: a host that stops answering has the oldest freed
|
|
344
|
+
* anyway, the one it is likeliest to have looked up already.
|
|
345
|
+
*
|
|
346
|
+
* Bridges before 0.4 have no `releaseSurface`; there the finalizer is
|
|
347
|
+
* still the only owner, and this is the drop it always was.
|
|
196
348
|
*/
|
|
197
349
|
_releaseRing() {
|
|
198
350
|
const ring = this._ring;
|
|
@@ -201,7 +353,58 @@ export class CocoaPaneWindow {
|
|
|
201
353
|
if (!ring) return;
|
|
202
354
|
const release = this._native.releaseSurface;
|
|
203
355
|
if (typeof release !== 'function') return;
|
|
204
|
-
for (const s of ring)
|
|
356
|
+
for (const s of ring) {
|
|
357
|
+
if (s.presentedSeq > this._shownSeq) this._retired.push(s);
|
|
358
|
+
else release.call(this._native, s.handle);
|
|
359
|
+
}
|
|
360
|
+
while (this._retired.length > CocoaPaneWindow.RING) {
|
|
361
|
+
release.call(this._native, this._retired.shift().handle);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The host has looked up every present up to `seq` (`pane-shown`): the
|
|
367
|
+
* retired buffers they named are its now, or nobody's, and go.
|
|
368
|
+
*/
|
|
369
|
+
_shown(seq) {
|
|
370
|
+
if (!(seq > this._shownSeq)) return;
|
|
371
|
+
// the first of a run of answers: what was looked up before it
|
|
372
|
+
if (!this._kickQueued) this._shownBefore = this._shownSeq;
|
|
373
|
+
this._shownSeq = seq;
|
|
374
|
+
this._shownAt = performance.now();
|
|
375
|
+
if (this._retired.length > 0) {
|
|
376
|
+
const release = this._native.releaseSurface;
|
|
377
|
+
this._retired = this._retired.filter((s) => {
|
|
378
|
+
if (s.presentedSeq > seq) return true;
|
|
379
|
+
release.call(this._native, s.handle);
|
|
380
|
+
return false;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// A frame the clock held for want of a buffer (`frameHeld`) goes now,
|
|
384
|
+
// not on the next pump tick — once every answer that came with this one
|
|
385
|
+
// is in. A host back from a long frame answers a run of presents at
|
|
386
|
+
// once, the channel hands them over in one turn, and a frame run on the
|
|
387
|
+
// first found only the buffer the host had shown all along.
|
|
388
|
+
if (this._kickQueued) return;
|
|
389
|
+
this._kickQueued = true;
|
|
390
|
+
queueMicrotask(() => {
|
|
391
|
+
this._kickQueued = false;
|
|
392
|
+
const app = this.app;
|
|
393
|
+
if (!this.destroyed && app._rafQueue.some((e) => e.wnd === this)) {
|
|
394
|
+
app._tickFrames();
|
|
395
|
+
app._presentAll();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Every retired buffer, answered or not: the window is going, and so is
|
|
401
|
+
* the layer the host showed it on. */
|
|
402
|
+
_releaseRetired() {
|
|
403
|
+
const retired = this._retired;
|
|
404
|
+
this._retired = [];
|
|
405
|
+
const release = this._native.releaseSurface;
|
|
406
|
+
if (typeof release !== 'function') return;
|
|
407
|
+
for (const s of retired) release.call(this._native, s.handle);
|
|
205
408
|
}
|
|
206
409
|
|
|
207
410
|
_ensureSurface() {
|
|
@@ -230,10 +433,88 @@ export class CocoaPaneWindow {
|
|
|
230
433
|
// decided when the flush reports its rects — see CocoaWindow's
|
|
231
434
|
// `_ensureSurface` for why not a queued full frame from here
|
|
232
435
|
if (hadSurface) this._freshSurface = true;
|
|
436
|
+
} else if (!this._surface) {
|
|
437
|
+
this._takeBack();
|
|
233
438
|
}
|
|
234
439
|
return this._surface;
|
|
235
440
|
}
|
|
236
441
|
|
|
442
|
+
/**
|
|
443
|
+
* The buffer the frame about to be drawn goes into, taken at its first
|
|
444
|
+
* draw rather than at the present before it, so the host has had as long
|
|
445
|
+
* as it can to answer — CocoaWindow's `_takeBack`, over a ring the host
|
|
446
|
+
* reads from another process.
|
|
447
|
+
*
|
|
448
|
+
* One the host is done with (`_done`): a present after the last one that
|
|
449
|
+
* named it has been looked up, so it is neither on the layer nor on its
|
|
450
|
+
* way there. Of those, one the WindowServer has let go of as well —
|
|
451
|
+
* `surfaceIsInUse` false: it reads a buffer for a refresh or two after
|
|
452
|
+
* the layer has another, 8ms after the pane hears the answer at the
|
|
453
|
+
* median and 22 at the 90th percentile in the browser example — and of
|
|
454
|
+
* those the one presented longest ago. The clock holds a frame that would
|
|
455
|
+
* find none (`frameHeld`). One that comes anyway, once the WindowServer
|
|
456
|
+
* has held on for a frame interval, takes one it still reads — but the
|
|
457
|
+
* one the screen showed before the host's latest answers last, which a
|
|
458
|
+
* host back from a long frame has yet to commit the replacement for; and
|
|
459
|
+
* once the host is overdue, it takes a buffer whose present the host has
|
|
460
|
+
* yet to look up
|
|
461
|
+
* rather than the one on the layer: a host that comes back looks up
|
|
462
|
+
* everything queued in one turn, and only the last of it reaches glass,
|
|
463
|
+
* where the buffer on the layer is on glass now.
|
|
464
|
+
*
|
|
465
|
+
* Then the frame just presented, copied across whole: in a ring of three
|
|
466
|
+
* the buffer taken was last drawn two presents ago or more, and is stale
|
|
467
|
+
* everywhere. After a full frame as much as a partial one: what a frame
|
|
468
|
+
* was says nothing of what the next will be, and skipping the copy there
|
|
469
|
+
* had a partial frame after a full one — a status line, a hover's
|
|
470
|
+
* underline — drawn over the page as it was two frames back. The copy is
|
|
471
|
+
* wasted only where the next frame is full as well.
|
|
472
|
+
*/
|
|
473
|
+
_takeBack() {
|
|
474
|
+
const ring = this._ring;
|
|
475
|
+
const last = ring[this._shownIndex];
|
|
476
|
+
let pick = -1;
|
|
477
|
+
let rank = Infinity;
|
|
478
|
+
for (let i = 0; i < ring.length; i += 1) {
|
|
479
|
+
const s = ring[i];
|
|
480
|
+
if (s === last) continue;
|
|
481
|
+
// done and let go of, then done, then on its way, then on the layer:
|
|
482
|
+
// the oldest of the best — but of the done, the one the screen showed
|
|
483
|
+
// before the host's latest answers last
|
|
484
|
+
const kind = this._done(s)
|
|
485
|
+
? this._held(s)
|
|
486
|
+
? 1 + Number(s.presentedSeq === this._shownBefore)
|
|
487
|
+
: 0
|
|
488
|
+
: s.presentedSeq > this._shownSeq
|
|
489
|
+
? 3
|
|
490
|
+
: 4;
|
|
491
|
+
const r = kind * 2 ** 32 + (s.presentedSeq ?? 0);
|
|
492
|
+
if (r < rank) {
|
|
493
|
+
rank = r;
|
|
494
|
+
pick = i;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const back = ring[pick];
|
|
498
|
+
this._drawIndex = pick;
|
|
499
|
+
this._surface = back.handle;
|
|
500
|
+
// a different native surface owns the graphics state now — the context
|
|
501
|
+
// re-syncs its sticky state off the generation
|
|
502
|
+
this._surfaceGen++;
|
|
503
|
+
this._native.surfaceLock(back.handle);
|
|
504
|
+
if (last) this._native.copySurfaceRegion(last.handle, back.handle, null);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Whether the WindowServer still reads `buffer`: `IOSurfaceIsInUse`,
|
|
508
|
+
* which counts the host's render server from this process too. A bridge
|
|
509
|
+
* that cannot say (before 0.10) is answered no. */
|
|
510
|
+
_held(buffer) {
|
|
511
|
+
const inUse = this._native.surfaceIsInUse;
|
|
512
|
+
return (
|
|
513
|
+
typeof inUse === 'function' &&
|
|
514
|
+
inUse.call(this._native, buffer.handle) === true
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
237
518
|
getContext() {
|
|
238
519
|
if (!this._ctx) {
|
|
239
520
|
this._ctx = new BackendContext2D(
|
|
@@ -278,10 +559,12 @@ export class CocoaPaneWindow {
|
|
|
278
559
|
}
|
|
279
560
|
|
|
280
561
|
scrollRegion(rect, dx, dy) {
|
|
281
|
-
if (!this.
|
|
562
|
+
if (!this._ring) return false;
|
|
282
563
|
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
564
|
+
// the frame's first draw, which takes its buffer (`_takeBack`)
|
|
565
|
+
const surface = this._ensureSurface();
|
|
283
566
|
const moved = this._native.scrollSurface(
|
|
284
|
-
|
|
567
|
+
surface,
|
|
285
568
|
Math.round(rect.x),
|
|
286
569
|
Math.round(rect.y),
|
|
287
570
|
Math.round(rect.width),
|
|
@@ -301,33 +584,28 @@ export class CocoaPaneWindow {
|
|
|
301
584
|
const shown = this._ring[this._drawIndex];
|
|
302
585
|
this._native.surfaceUnlock(shown.handle);
|
|
303
586
|
this._flushDamage = null;
|
|
587
|
+
// the buffer is named in the channel until the host says otherwise
|
|
588
|
+
// (`_releaseRing`, `_done`)
|
|
589
|
+
shown.presentedSeq = ++this._seq;
|
|
590
|
+
this._presentedAt = performance.now();
|
|
591
|
+
this._shownIndex = this._drawIndex;
|
|
592
|
+
// the next frame takes its buffer when it draws (`_takeBack`)
|
|
593
|
+
this._drawIndex = -1;
|
|
594
|
+
this._surface = null;
|
|
595
|
+
// last: the answer runs a held frame (`_shown`), which must find this
|
|
596
|
+
// present made
|
|
304
597
|
this._post({
|
|
305
598
|
type: 'pane-present',
|
|
306
|
-
seq:
|
|
599
|
+
seq: this._seq,
|
|
307
600
|
id: shown.iosurfaceId,
|
|
308
601
|
width: this.width,
|
|
309
602
|
height: this.height,
|
|
310
603
|
});
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const next = this._ring[this._drawIndex];
|
|
317
|
-
this._surface = next.handle;
|
|
318
|
-
this._surfaceGen++;
|
|
319
|
-
this._native.surfaceLock(next.handle);
|
|
320
|
-
// The next frame may paint only its damage, so `next` must first hold
|
|
321
|
-
// the last complete frame underneath — and the WHOLE of it, not just
|
|
322
|
-
// this frame's rects, because in a three-buffer ring `next` was last
|
|
323
|
-
// drawn two presents ago and is stale everywhere. A full copy of the
|
|
324
|
-
// just-shown buffer is the complete background; the safe target is what
|
|
325
|
-
// triple buffering buys. After a full frame as much as a partial one:
|
|
326
|
-
// what a frame was says nothing of what the next will be, and skipping
|
|
327
|
-
// the copy there had a partial frame after a full one — a status line,
|
|
328
|
-
// a hover's underline — drawn over the page as it was two frames back.
|
|
329
|
-
// The copy is wasted only where the next frame is full as well.
|
|
330
|
-
this._native.copySurfaceRegion(shown.handle, next.handle, null);
|
|
604
|
+
// and the size the host last sent is on screen (`pane-sized`)
|
|
605
|
+
if (this._owesSized) {
|
|
606
|
+
this._owesSized = false;
|
|
607
|
+
this._post({ type: 'pane-sized' });
|
|
608
|
+
}
|
|
331
609
|
}
|
|
332
610
|
|
|
333
611
|
snapshot() {
|
|
@@ -338,7 +616,10 @@ export class CocoaPaneWindow {
|
|
|
338
616
|
if (this.destroyed) return;
|
|
339
617
|
this.destroyed = true;
|
|
340
618
|
this.app._unregisterWindow(this);
|
|
619
|
+
clearTimeout(this._pollTimer);
|
|
620
|
+
this._pollTimer = null;
|
|
341
621
|
this._releaseRing();
|
|
622
|
+
this._releaseRetired();
|
|
342
623
|
}
|
|
343
624
|
}
|
|
344
625
|
|
package/src/cocoa/window.js
CHANGED
|
@@ -975,7 +975,15 @@ export class CocoaWindow {
|
|
|
975
975
|
* painting a state the next event in the same batch replaces.
|
|
976
976
|
*/
|
|
977
977
|
frameInFlight() {
|
|
978
|
-
|
|
978
|
+
return this.app._batching || this.frameHeld();
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Whether the frame clock holds this window's next frame
|
|
983
|
+
* (`CocoaApp._tickFrames`): the fence half of `frameInFlight`, without the
|
|
984
|
+
* batch, which the clock never ticks inside.
|
|
985
|
+
*/
|
|
986
|
+
frameHeld() {
|
|
979
987
|
return (
|
|
980
988
|
this._awaiting != null &&
|
|
981
989
|
this._surfaceSize?.width === this.width &&
|
package/src/events.js
CHANGED
|
@@ -1672,6 +1672,18 @@ export class EventManager {
|
|
|
1672
1672
|
// its ordinary turn.
|
|
1673
1673
|
if (step.consumed) return;
|
|
1674
1674
|
}
|
|
1675
|
+
// A key that typed more than one character has no code point to type
|
|
1676
|
+
// them with: a backend whose keyboard layout composes on its own says
|
|
1677
|
+
// so with `text` — the accent and a letter it does not combine with,
|
|
1678
|
+
// `´s` (src/cocoa/keymap.js). Committed the way an input method's text
|
|
1679
|
+
// is, a composition that opens and closes on this key, which is where
|
|
1680
|
+
// an element that types inserts it — and like a composed key, it is
|
|
1681
|
+
// the key's whole answer.
|
|
1682
|
+
if (native.text && target.composes !== false) {
|
|
1683
|
+
this._composition('Start', target, '', native);
|
|
1684
|
+
this._composition('End', target, String(native.text), native);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1675
1687
|
// The element's own behaviour first, focus traversal after it — Tab is
|
|
1676
1688
|
// an ordinary defaultable key rather than one the focus manager eats on
|
|
1677
1689
|
// the way past. Cycling first meant an editor could only keep Tab as an
|
package/src/frame/index.js
CHANGED
|
@@ -192,6 +192,11 @@ function shallowEqual(a, b) {
|
|
|
192
192
|
|
|
193
193
|
const EMPTY_PROPS = {};
|
|
194
194
|
|
|
195
|
+
/** How long a rect sent to a pane that acknowledges sizes waits for its
|
|
196
|
+
* `pane-sized` before the newest is sent anyway: a frame of a slow page,
|
|
197
|
+
* several times over, so a lost answer costs a pause and not the size. */
|
|
198
|
+
export const PANE_SIZED_WAIT_MS = 500;
|
|
199
|
+
|
|
195
200
|
/**
|
|
196
201
|
* A module of this application, mounted in its own process, its window
|
|
197
202
|
* embedded here.
|
|
@@ -552,7 +557,54 @@ function PaneHostView({
|
|
|
552
557
|
// moves when the box does and when the window does, so it is sent on
|
|
553
558
|
// layout, on a scroll that shifts the box, and on every anchor change
|
|
554
559
|
// of the window, which a move of the window is.
|
|
560
|
+
//
|
|
561
|
+
// **The newest rect wins.** A drag sends the host a size a frame, and a
|
|
562
|
+
// pane whose page takes longer than that to lay out was sent them
|
|
563
|
+
// faster than it could take them: a pipe hands a pane process one
|
|
564
|
+
// message a turn of its loop, with a frame in between, so the pane laid
|
|
565
|
+
// out and painted every size the drag passed through, one a frame, and
|
|
566
|
+
// went on resizing seconds after the edge stopped. A pane that says it
|
|
567
|
+
// has painted a size (`pane-sized`) is sent one rect at a time: what
|
|
568
|
+
// the host comes to while one is out replaces what was waiting, and goes
|
|
569
|
+
// when the pane says it has painted the last. A pane that never says so
|
|
570
|
+
// is sent every rect, as before, and one that goes quiet is sent the
|
|
571
|
+
// newest anyway after `PANE_SIZED_WAIT_MS`.
|
|
555
572
|
let sent = null;
|
|
573
|
+
let pending = null;
|
|
574
|
+
let acks = false;
|
|
575
|
+
let inFlight = false;
|
|
576
|
+
let waiting = null;
|
|
577
|
+
const transmit = (rect) => {
|
|
578
|
+
sent = rect;
|
|
579
|
+
s.trySend?.({
|
|
580
|
+
type: 'pane-rect',
|
|
581
|
+
width: rect.width,
|
|
582
|
+
height: rect.height,
|
|
583
|
+
scale: rect.scale,
|
|
584
|
+
...(rect.screen && { screen: rect.screen }),
|
|
585
|
+
});
|
|
586
|
+
if (!acks) return;
|
|
587
|
+
inFlight = true;
|
|
588
|
+
if (waiting) clearTimeout(waiting);
|
|
589
|
+
waiting = setTimeout(sized, PANE_SIZED_WAIT_MS);
|
|
590
|
+
};
|
|
591
|
+
const sameRect = (a, b) =>
|
|
592
|
+
!!a &&
|
|
593
|
+
!!b &&
|
|
594
|
+
a.width === b.width &&
|
|
595
|
+
a.height === b.height &&
|
|
596
|
+
a.scale === b.scale &&
|
|
597
|
+
a.screen?.x === b.screen?.x &&
|
|
598
|
+
a.screen?.y === b.screen?.y;
|
|
599
|
+
const sized = () => {
|
|
600
|
+
if (waiting) clearTimeout(waiting);
|
|
601
|
+
waiting = null;
|
|
602
|
+
inFlight = false;
|
|
603
|
+
const next = pending;
|
|
604
|
+
pending = null;
|
|
605
|
+
// a drag that came back to where it was sent from waits for nothing
|
|
606
|
+
if (next && !sameRect(next, sent)) transmit(next);
|
|
607
|
+
};
|
|
556
608
|
const syncSize = () => {
|
|
557
609
|
host.setRect(node.abs);
|
|
558
610
|
const sc = node.scale ?? 1;
|
|
@@ -565,23 +617,10 @@ function PaneHostView({
|
|
|
565
617
|
y: Math.round(origin.y + node.abs.y),
|
|
566
618
|
}
|
|
567
619
|
: null;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
sent.screen?.x === screen?.x &&
|
|
573
|
-
sent.screen?.y === screen?.y
|
|
574
|
-
) {
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
sent = { width, height, screen };
|
|
578
|
-
s.trySend?.({
|
|
579
|
-
type: 'pane-rect',
|
|
580
|
-
width,
|
|
581
|
-
height,
|
|
582
|
-
scale: sc,
|
|
583
|
-
...(screen && { screen }),
|
|
584
|
-
});
|
|
620
|
+
const rect = { width, height, scale: sc, screen };
|
|
621
|
+
if (sameRect(rect, pending ?? sent)) return;
|
|
622
|
+
if (inFlight) pending = rect;
|
|
623
|
+
else transmit(rect);
|
|
585
624
|
};
|
|
586
625
|
const origAbsolutize = node.absolutize.bind(node);
|
|
587
626
|
node.absolutize = (ox, oy) => {
|
|
@@ -625,6 +664,11 @@ function PaneHostView({
|
|
|
625
664
|
} else if (msg?.type === 'pane-present') {
|
|
626
665
|
host.setRect(node.abs);
|
|
627
666
|
host.present(msg.id, msg);
|
|
667
|
+
} else if (msg?.type === 'pane-sized') {
|
|
668
|
+
// the pane has painted the rect it was last sent: the newest one
|
|
669
|
+
// waiting goes now, and from here on one is out at a time
|
|
670
|
+
acks = true;
|
|
671
|
+
sized();
|
|
628
672
|
} else if (msg?.type === 'pane-cursor') {
|
|
629
673
|
// The pane names its cursor as an element that draws names one for
|
|
630
674
|
// a point: this box is the node the host's pointer is over, so the
|
|
@@ -637,6 +681,8 @@ function PaneHostView({
|
|
|
637
681
|
});
|
|
638
682
|
return () => {
|
|
639
683
|
off();
|
|
684
|
+
if (waiting) clearTimeout(waiting);
|
|
685
|
+
waiting = null;
|
|
640
686
|
offAnchor?.();
|
|
641
687
|
offFocus?.();
|
|
642
688
|
node.defaultCursor = undefined;
|