react-x11 2.5.0 → 2.6.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/package.json +4 -3
- package/src/Reconciler.js +6 -0
- package/src/cocoa/app.js +199 -12
- package/src/cocoa/context2d.js +16 -1
- package/src/cocoa/panehost.js +20 -2
- package/src/cocoa/panewindow.js +61 -9
- package/src/cocoa/presenter.js +191 -16
- package/src/cocoa/surface.js +9 -3
- package/src/cocoa/window.js +123 -13
- package/src/events.js +23 -5
- package/src/index.d.ts +25 -0
- package/src/nodes.js +849 -138
- package/src/paintcache.js +71 -11
- package/src/svgnodes.js +4 -3
- package/src/types/elements.d.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"docs:dev": "npm --prefix website start",
|
|
67
67
|
"docs:build": "npm --prefix website run build",
|
|
68
68
|
"docs:test": "npm --prefix website test",
|
|
69
|
-
"bench:presenters": "node --import tsx scripts/bench/presenters.js"
|
|
69
|
+
"bench:presenters": "node --import tsx scripts/bench/presenters.js",
|
|
70
|
+
"bench:touched": "node scripts/bench/touched.js"
|
|
70
71
|
},
|
|
71
72
|
"repository": {
|
|
72
73
|
"type": "git",
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
"yoga-layout": "^3.2.1"
|
|
91
92
|
},
|
|
92
93
|
"optionalDependencies": {
|
|
93
|
-
"@windowkit/appkit": "^0.
|
|
94
|
+
"@windowkit/appkit": "^0.4.0",
|
|
94
95
|
"dbus-native": "^0.15.1",
|
|
95
96
|
"x11-dri": "^0.7.0"
|
|
96
97
|
},
|
package/src/Reconciler.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
flushWindowMaps,
|
|
32
32
|
flushWindowRestacks,
|
|
33
33
|
windowAttributes,
|
|
34
|
+
setTextStripBelow,
|
|
34
35
|
} from './nodes.js';
|
|
35
36
|
import { hasDropProps } from './dnd.js';
|
|
36
37
|
import { AppProvider } from './appcontext.js';
|
|
@@ -790,6 +791,11 @@ export async function createRoot(options = {}) {
|
|
|
790
791
|
// anything from disk.
|
|
791
792
|
beginCompose(app, rest.compose);
|
|
792
793
|
|
|
794
|
+
// Under what size a paragraph is painted as a strip of its ink instead
|
|
795
|
+
// of as glyphs (nodes.js, `TextNode._paintsStrip`): six logical pixels by
|
|
796
|
+
// default, 0 for glyphs at every size.
|
|
797
|
+
setTextStripBelow(app, rest.textStripBelow);
|
|
798
|
+
|
|
793
799
|
// Whether a subtree coming back out of hiding — a `<Suspense>` boundary
|
|
794
800
|
// resolving, an `<Activity>` shown again — takes the keyboard back with it
|
|
795
801
|
// (src/events.js, `subtreeRevealed`). On by default; `false` is the
|
package/src/cocoa/app.js
CHANGED
|
@@ -29,7 +29,18 @@ import { CocoaWindow } from './window.js';
|
|
|
29
29
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
30
30
|
import { loadNative } from './native.js';
|
|
31
31
|
|
|
32
|
+
// The frame interval: how often a scheduled frame may paint, in ms. The
|
|
33
|
+
// default is the period of the display the window is on — `listScreens`
|
|
34
|
+
// reports each panel's `fps` (NSScreen.maximumFramesPerSecond, bridge 0.4),
|
|
35
|
+
// so a 120Hz panel paints every 8.3ms and a 60Hz monitor every 16.7 —
|
|
36
|
+
// and `createRoot({ cocoa: { frameInterval } })` overrides it for every
|
|
37
|
+
// window. 16 stands in where the OS cannot say (`fps: 0`) and under a
|
|
38
|
+
// bridge that does not report it: a 60Hz floor every Mac clears.
|
|
32
39
|
const RAF_INTERVAL_MS = 16;
|
|
40
|
+
const PUMP_INTERVAL_MS = 8;
|
|
41
|
+
// How early a pump tick may take a frame that is not quite due, in ms — the
|
|
42
|
+
// drift of a timer, not a fraction of the pump (`_frameDue`).
|
|
43
|
+
const FRAME_SLACK_MS = 1;
|
|
33
44
|
|
|
34
45
|
export class CocoaApp {
|
|
35
46
|
constructor(native, options = {}) {
|
|
@@ -37,8 +48,18 @@ export class CocoaApp {
|
|
|
37
48
|
this.options = options;
|
|
38
49
|
this._windows = new Map(); // windowNumber -> CocoaWindow
|
|
39
50
|
this._grabWindow = null;
|
|
40
|
-
this._rafQueue = [];
|
|
51
|
+
this._rafQueue = []; // [{ cb, wnd }]
|
|
52
|
+
// the app's own frame clock, for a frame no window owns (a pane's)
|
|
41
53
|
this._rafLast = 0;
|
|
54
|
+
// an explicit interval applies to every window; null means each
|
|
55
|
+
// window paces itself on its own display (`frameIntervalFor`)
|
|
56
|
+
this._frameInterval = options.cocoa?.frameInterval ?? null;
|
|
57
|
+
this._pumpInterval = PUMP_INTERVAL_MS;
|
|
58
|
+
// the one-shot that runs a frame due between two pump ticks, and when
|
|
59
|
+
// it is due (`_armFrameTimer`)
|
|
60
|
+
this._frameTimer = null;
|
|
61
|
+
this._frameTimerAt = 0;
|
|
62
|
+
this._geometryFlushQueued = false;
|
|
42
63
|
this._shadowStale = new Set();
|
|
43
64
|
this._pump = null;
|
|
44
65
|
this._closed = false;
|
|
@@ -243,6 +264,35 @@ export class CocoaApp {
|
|
|
243
264
|
this._windows.set(wnd.windowNumber, wnd);
|
|
244
265
|
}
|
|
245
266
|
|
|
267
|
+
/**
|
|
268
|
+
* How often `wnd` may paint, in ms: the explicit `frameInterval` when the
|
|
269
|
+
* root was given one, else the period of the screen under the window's
|
|
270
|
+
* centre — the display's own rate is the only honest cadence, and on a
|
|
271
|
+
* desk with a 120Hz panel and a 60Hz monitor the two windows differ. A
|
|
272
|
+
* window on no screen (mid-drag between two, or off the edge) and a
|
|
273
|
+
* screen the OS reports no rate for take the primary's, then 16ms.
|
|
274
|
+
*/
|
|
275
|
+
frameIntervalFor(wnd) {
|
|
276
|
+
if (this._frameInterval != null) return this._frameInterval;
|
|
277
|
+
const s = this.scale;
|
|
278
|
+
const screens = this._screens ?? [];
|
|
279
|
+
let screen = null;
|
|
280
|
+
if (wnd) {
|
|
281
|
+
const cx = (wnd.x + wnd.width / 2) / s;
|
|
282
|
+
const cy = (wnd.y + wnd.height / 2) / s;
|
|
283
|
+
screen =
|
|
284
|
+
screens.find(
|
|
285
|
+
(sc) =>
|
|
286
|
+
cx >= sc.x &&
|
|
287
|
+
cx < sc.x + sc.width &&
|
|
288
|
+
cy >= sc.y &&
|
|
289
|
+
cy < sc.y + sc.height,
|
|
290
|
+
) ?? null;
|
|
291
|
+
}
|
|
292
|
+
const fps = screen?.fps || screens[0]?.fps || 0;
|
|
293
|
+
return fps > 0 ? 1000 / fps : RAF_INTERVAL_MS;
|
|
294
|
+
}
|
|
295
|
+
|
|
246
296
|
/**
|
|
247
297
|
* The pane's end of the frame channel (childmain hands it over,
|
|
248
298
|
* feature-detected so the X11 pane path never notices): geometry and
|
|
@@ -277,8 +327,9 @@ export class CocoaApp {
|
|
|
277
327
|
|
|
278
328
|
// --- the pump ------------------------------------------------------------
|
|
279
329
|
|
|
280
|
-
start({ pumpInterval =
|
|
330
|
+
start({ pumpInterval = PUMP_INTERVAL_MS } = {}) {
|
|
281
331
|
if (this._pump) return;
|
|
332
|
+
this._pumpInterval = pumpInterval;
|
|
282
333
|
const native = this._native;
|
|
283
334
|
// A pane process has no NSApplication to pump — no windows, no events,
|
|
284
335
|
// no dock presence. Its loop is frames and presents only.
|
|
@@ -292,6 +343,7 @@ export class CocoaApp {
|
|
|
292
343
|
native.initApp();
|
|
293
344
|
native.setBackendEventCallback((ev) => this._route(ev));
|
|
294
345
|
this._pump = setInterval(() => {
|
|
346
|
+
this._endLiveResizes();
|
|
295
347
|
native.pump2(); // flushes the previous tick's CATransaction
|
|
296
348
|
if (this._shadowStale.size) {
|
|
297
349
|
for (const wnd of this._shadowStale) {
|
|
@@ -304,27 +356,129 @@ export class CocoaApp {
|
|
|
304
356
|
}, pumpInterval);
|
|
305
357
|
}
|
|
306
358
|
|
|
307
|
-
|
|
308
|
-
|
|
359
|
+
/**
|
|
360
|
+
* A pump tick means no modal loop owns the thread, so no window is being
|
|
361
|
+
* resized live right now: the flag the delegate set on the last tick of a
|
|
362
|
+
* drag comes off here, ahead of the frames that tick, and the catch-up
|
|
363
|
+
* frame a deferred layout owes (nodes.js) runs on this very tick.
|
|
364
|
+
*/
|
|
365
|
+
_endLiveResizes() {
|
|
366
|
+
for (const wnd of this._windows.values()) wnd.liveResizing = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_requestFrame(cb, wnd = null) {
|
|
370
|
+
this._rafQueue.push({ cb, wnd });
|
|
309
371
|
return this._rafQueue.length;
|
|
310
372
|
}
|
|
311
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Whether `clock` — a window, or the app for a frame no window owns —
|
|
376
|
+
* is due a frame at `now`, and if so, stamps it.
|
|
377
|
+
*
|
|
378
|
+
* The clock keeps the display's period, not the pump's: a frame that ran
|
|
379
|
+
* is stamped one interval after the last one was due rather than at the
|
|
380
|
+
* moment it happened to run, so a tick that took it a little early or a
|
|
381
|
+
* timer that fired a little late moves nothing — the next frame is still
|
|
382
|
+
* due where the display's next refresh is. A frame that arrives more than
|
|
383
|
+
* an interval late (a slow one) re-anchors the clock instead of owing the
|
|
384
|
+
* frames it missed.
|
|
385
|
+
*
|
|
386
|
+
* The gate is a millisecond of slack under the interval, which is a
|
|
387
|
+
* timer's drift and no more. It used to be half a pump, so that a frame
|
|
388
|
+
* would land on the first tick at or after the interval instead of
|
|
389
|
+
* alternating between the tick before and the one after; that quantized
|
|
390
|
+
* the period to the pump — a 75Hz monitor's 13.3ms gate of 9.3 fell on
|
|
391
|
+
* the 16ms tick, and the window painted at 62.5fps. A frame due between
|
|
392
|
+
* two ticks now gets a timer of its own (`_armFrameTimer`), and the
|
|
393
|
+
* period is the display's whatever the pump's cadence.
|
|
394
|
+
*/
|
|
395
|
+
_frameDue(clock, now) {
|
|
396
|
+
const interval =
|
|
397
|
+
clock === this ? this.frameIntervalFor(null) : clock._frameInterval;
|
|
398
|
+
const since = now - clock._rafLast;
|
|
399
|
+
if (since < interval - FRAME_SLACK_MS) return false;
|
|
400
|
+
clock._rafLast = since < 2 * interval ? clock._rafLast + interval : now;
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** How long until `clock` is due, from `now`. */
|
|
405
|
+
_frameWait(clock, now) {
|
|
406
|
+
const interval =
|
|
407
|
+
clock === this ? this.frameIntervalFor(null) : clock._frameInterval;
|
|
408
|
+
return interval - FRAME_SLACK_MS - (now - clock._rafLast);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* A frame that falls between two pump ticks gets a tick of its own: a
|
|
413
|
+
* one-shot timer at the moment it is due, which runs the frame queue and
|
|
414
|
+
* presents what it painted. Only when the next pump tick would be late
|
|
415
|
+
* for it — a frame due after that tick waits for the tick, which decides
|
|
416
|
+
* again. One timer at a time, at the soonest of what is owed; a pump tick
|
|
417
|
+
* that comes first runs whatever is due and re-arms for the rest.
|
|
418
|
+
*
|
|
419
|
+
* The timer costs the frame nothing the tick does not: a present commits
|
|
420
|
+
* its own transaction (the bridge flushes on the flip), so what is
|
|
421
|
+
* painted here is on glass without waiting for the next pump. What the
|
|
422
|
+
* tick alone still does is pump AppKit's events, which is what
|
|
423
|
+
* `pumpInterval` stays the cadence of.
|
|
424
|
+
*/
|
|
425
|
+
_armFrameTimer(wait, now) {
|
|
426
|
+
if (!(wait > 0 && wait < this._pumpInterval)) return;
|
|
427
|
+
const at = now + wait;
|
|
428
|
+
if (this._frameTimer) {
|
|
429
|
+
if (at >= this._frameTimerAt) return;
|
|
430
|
+
clearTimeout(this._frameTimer);
|
|
431
|
+
}
|
|
432
|
+
this._frameTimerAt = at;
|
|
433
|
+
this._frameTimer = setTimeout(
|
|
434
|
+
() => {
|
|
435
|
+
this._frameTimer = null;
|
|
436
|
+
if (this._closed) return;
|
|
437
|
+
this._tickFrames();
|
|
438
|
+
this._presentAll();
|
|
439
|
+
},
|
|
440
|
+
Math.max(1, Math.round(wait)),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
312
444
|
_tickFrames() {
|
|
313
445
|
if (!this._rafQueue.length) return;
|
|
314
|
-
const now =
|
|
315
|
-
|
|
316
|
-
|
|
446
|
+
const now = performance.now();
|
|
447
|
+
// Each window keeps its own clock, so a window on a 120Hz panel paints
|
|
448
|
+
// every refresh while one on a 60Hz monitor paints every other pump
|
|
449
|
+
// tick. Decided once per clock per tick: every frame a window queued
|
|
450
|
+
// runs when it is due, not just the first.
|
|
451
|
+
const due = new Map();
|
|
317
452
|
const queue = this._rafQueue;
|
|
318
453
|
this._rafQueue = [];
|
|
319
|
-
|
|
454
|
+
let soonest = Infinity;
|
|
455
|
+
for (const entry of queue) {
|
|
456
|
+
const clock = entry.wnd ?? this;
|
|
457
|
+
if (!due.has(clock)) due.set(clock, this._frameDue(clock, now));
|
|
458
|
+
if (!due.get(clock)) {
|
|
459
|
+
this._rafQueue.push(entry);
|
|
460
|
+
soonest = Math.min(soonest, this._frameWait(clock, now));
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
// A window nobody can see owes no frame: its callback waits here until
|
|
464
|
+
// it is back on glass, and the damage it answers accumulates on the
|
|
465
|
+
// node — one catch-up frame then, instead of a full paint per tick
|
|
466
|
+
// into a backing store no one reads. `_visible` is the window's own
|
|
467
|
+
// rule (mapped, not ordered out, not miniaturized, not entirely
|
|
468
|
+
// behind another application's window).
|
|
469
|
+
if (entry.wnd && !entry.wnd._visible()) {
|
|
470
|
+
this._rafQueue.push(entry);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
320
473
|
try {
|
|
321
|
-
cb(now);
|
|
474
|
+
entry.cb(now);
|
|
322
475
|
} catch (err) {
|
|
323
476
|
queueMicrotask(() => {
|
|
324
477
|
throw err;
|
|
325
478
|
});
|
|
326
479
|
}
|
|
327
480
|
}
|
|
481
|
+
this._armFrameTimer(soonest, now);
|
|
328
482
|
}
|
|
329
483
|
|
|
330
484
|
_presentAll() {
|
|
@@ -366,6 +520,8 @@ export class CocoaApp {
|
|
|
366
520
|
return this._routeFocus(ev, 'focus');
|
|
367
521
|
case 'window-blur':
|
|
368
522
|
return this._routeFocus(ev, 'blur');
|
|
523
|
+
case 'window-occlusion':
|
|
524
|
+
return this._routeOcclusion(ev);
|
|
369
525
|
case 'menu-activate':
|
|
370
526
|
this._activeGlobalMenu?.activate(ev.id);
|
|
371
527
|
return this._afterInput();
|
|
@@ -459,6 +615,13 @@ export class CocoaApp {
|
|
|
459
615
|
smooth: Boolean(ev.precise),
|
|
460
616
|
source: ev.precise ? 'valuator' : 'button',
|
|
461
617
|
});
|
|
618
|
+
// Painted now, like a press. On X11 the wheel is paced on the frame
|
|
619
|
+
// clock because ntk coalesces a touchpad's dozens of reports per frame
|
|
620
|
+
// into one event; AppKit already delivers scroll events at the
|
|
621
|
+
// display's rate, so answering each one is answering once per refresh
|
|
622
|
+
// — and answering it on the next frame tick instead was a 15ms median
|
|
623
|
+
// between the notch and the scroll, most of a refresh period of nothing.
|
|
624
|
+
this._afterInput();
|
|
462
625
|
}
|
|
463
626
|
|
|
464
627
|
_routeKey(ev) {
|
|
@@ -499,9 +662,31 @@ export class CocoaApp {
|
|
|
499
662
|
// handler returns, and the pump that would paint it is the thing the
|
|
500
663
|
// modal loop stalled. A second flush queued BEHIND that commit is what
|
|
501
664
|
// lets an open menu resize with the drag instead of on release.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
665
|
+
//
|
|
666
|
+
// One, not one per event: inside the modal loop no microtask runs until
|
|
667
|
+
// the drag ends, so a drag's every tick queued another — and they all
|
|
668
|
+
// ran on the release, each finding the frame the previous one had just
|
|
669
|
+
// paid. Coalesced, the release owes at most one flush.
|
|
670
|
+
if (!this._geometryFlushQueued) {
|
|
671
|
+
this._geometryFlushQueued = true;
|
|
672
|
+
queueMicrotask(() => {
|
|
673
|
+
this._geometryFlushQueued = false;
|
|
674
|
+
if (!this._closed) this._afterInput();
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* `windowDidChangeOcclusionState`: `visible` is "some pixel of the window
|
|
681
|
+
* is on glass". Off, the window's frames wait in the queue and its
|
|
682
|
+
* present waits with them (`CocoaWindow._visible`); on, the next pump
|
|
683
|
+
* tick runs the catch-up frame and puts it on glass — no early flush,
|
|
684
|
+
* since nothing was asked for and the pump is at most one interval away.
|
|
685
|
+
*/
|
|
686
|
+
_routeOcclusion(ev) {
|
|
687
|
+
const wnd = this._window(ev);
|
|
688
|
+
if (!wnd || wnd.destroyed) return;
|
|
689
|
+
wnd._occluded = ev.visible === false;
|
|
505
690
|
}
|
|
506
691
|
|
|
507
692
|
_routeClose(ev) {
|
|
@@ -528,6 +713,8 @@ export class CocoaApp {
|
|
|
528
713
|
this._closed = true;
|
|
529
714
|
if (this._pump) clearInterval(this._pump);
|
|
530
715
|
this._pump = null;
|
|
716
|
+
if (this._frameTimer) clearTimeout(this._frameTimer);
|
|
717
|
+
this._frameTimer = null;
|
|
531
718
|
this._cocoaGL?.destroy();
|
|
532
719
|
this._cocoaGL = null;
|
|
533
720
|
this._native.setBackendEventCallback(null);
|
package/src/cocoa/context2d.js
CHANGED
|
@@ -13,9 +13,24 @@ import { cssColorStraight } from 'ntk';
|
|
|
13
13
|
|
|
14
14
|
const BLACK = [0, 0, 0, 1];
|
|
15
15
|
|
|
16
|
+
// A colour string is parsed once: a frame over a large tree sets the same
|
|
17
|
+
// few fills thousands of times, and the parse — a regex and four numbers —
|
|
18
|
+
// cost a third of `_applyFill` (measured on the presenter bench's `tiny`
|
|
19
|
+
// cell, 5,000 fills of two colours: 100ms of a 200ms frame). Bounded, and
|
|
20
|
+
// dropped whole rather than evicted, since a palette is a few dozen strings.
|
|
21
|
+
const parsedColors = new Map();
|
|
22
|
+
const PARSED_COLORS_MAX = 256;
|
|
23
|
+
|
|
16
24
|
function parseColor(value) {
|
|
17
25
|
if (value == null) return BLACK;
|
|
18
|
-
|
|
26
|
+
const key = typeof value === 'string' ? value : String(value);
|
|
27
|
+
let parsed = parsedColors.get(key);
|
|
28
|
+
if (parsed === undefined) {
|
|
29
|
+
parsed = cssColorStraight(key) ?? BLACK;
|
|
30
|
+
if (parsedColors.size >= PARSED_COLORS_MAX) parsedColors.clear();
|
|
31
|
+
parsedColors.set(key, parsed);
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
class LinearGradient {
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -36,10 +36,28 @@ export class CocoaPaneHost {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* A pane-present landed: scan out of the named shared surface.
|
|
41
|
+
*
|
|
42
|
+
* A present names a buffer the pane may since have retired. The channel
|
|
43
|
+
* is a queue and nothing acknowledges a present, so a pane-rect that
|
|
44
|
+
* changes the pane's size (or its scale — the host window moved to another
|
|
45
|
+
* display during startup, which is how this was first hit) can cross a
|
|
46
|
+
* present already in flight: the pane rebuilds its ring on that rect and
|
|
47
|
+
* releases the old one (`CocoaPaneWindow._ensureSurface`), and by the
|
|
48
|
+
* time the host looks the id up the surface is gone. That present is
|
|
49
|
+
* stale by construction — the pane's full frame on the fresh ring is
|
|
50
|
+
* queued behind it — so it is dropped, and the layer keeps the frame it
|
|
51
|
+
* already holds a reference to. Nothing else here throws; any other
|
|
52
|
+
* error is the bug it says it is.
|
|
53
|
+
*/
|
|
40
54
|
present(iosurfaceId) {
|
|
41
55
|
if (this.destroyed) return;
|
|
42
|
-
|
|
56
|
+
try {
|
|
57
|
+
this._native.setLayerContentsIOSurface(this.layer, iosurfaceId);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (!/IOSurfaceLookup/.test(err?.message ?? '')) throw err;
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
destroy() {
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -50,6 +50,7 @@ export class CocoaPaneWindow {
|
|
|
50
50
|
this._dirty = false;
|
|
51
51
|
this._flushDamage = 'full';
|
|
52
52
|
this._seq = 0;
|
|
53
|
+
this._presentedAt = -Infinity;
|
|
53
54
|
this._reactX11Node = null;
|
|
54
55
|
app._registerWindow(this);
|
|
55
56
|
}
|
|
@@ -98,8 +99,24 @@ export class CocoaPaneWindow {
|
|
|
98
99
|
return this.app._requestFrame(cb);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Whether the host may still be showing the frame before the last one —
|
|
104
|
+
* the gate `flushPendingFrames` (src/frames.js) is written around: a
|
|
105
|
+
* discrete input paints on the spot only when the last frame has landed,
|
|
106
|
+
* which is what folds a burst into one paced frame instead of a frame per
|
|
107
|
+
* event. On X11 the server says when a present was shown; a pane hears
|
|
108
|
+
* nothing back from the host, which flips the layer on its next pump tick
|
|
109
|
+
* and has Core Animation scan it out at the following refresh — so a
|
|
110
|
+
* present counts as in flight for one frame interval. Without a gate here
|
|
111
|
+
* every message the host queued while the pane was busy was answered
|
|
112
|
+
* with a full frame of its own: a forty-tick resize of a pane whose frame
|
|
113
|
+
* costs 300ms stepped through forty sizes for twelve seconds after the
|
|
114
|
+
* drag had ended, each one the previous surface stretched to the layer.
|
|
115
|
+
*/
|
|
101
116
|
frameInFlight() {
|
|
102
|
-
return
|
|
117
|
+
return (
|
|
118
|
+
performance.now() - this._presentedAt < this.app.frameIntervalFor(null)
|
|
119
|
+
);
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
// Three buffers, not two. A pane is cross-process: the host keeps
|
|
@@ -112,6 +129,28 @@ export class CocoaPaneWindow {
|
|
|
112
129
|
// windows need only two because Core Animation latches the front buffer.
|
|
113
130
|
static RING = 3;
|
|
114
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Free the ring now, not when V8 collects the handles — CocoaWindow's
|
|
134
|
+
* `_releaseBacking`, for three buffers instead of two: a host window
|
|
135
|
+
* drag resizes the pane a tick at a time, and each tick retires a ring
|
|
136
|
+
* that the finalizer would have held until a collection happened to run.
|
|
137
|
+
* The host keeps its own reference to whichever buffer its layer shows,
|
|
138
|
+
* so the frame on glass survives the free; a present of a retired buffer
|
|
139
|
+
* still in the channel finds no surface, and the host drops it
|
|
140
|
+
* (`CocoaPaneHost.present`) — the next present is the full frame on the
|
|
141
|
+
* new ring anyway. Bridges before 0.4 have no `releaseSurface`; there the
|
|
142
|
+
* finalizer is still the only owner, and this is the drop it always was.
|
|
143
|
+
*/
|
|
144
|
+
_releaseRing() {
|
|
145
|
+
const ring = this._ring;
|
|
146
|
+
this._ring = null;
|
|
147
|
+
this._surface = null;
|
|
148
|
+
if (!ring) return;
|
|
149
|
+
const release = this._native.releaseSurface;
|
|
150
|
+
if (typeof release !== 'function') return;
|
|
151
|
+
for (const s of ring) release.call(this._native, s.handle);
|
|
152
|
+
}
|
|
153
|
+
|
|
115
154
|
_ensureSurface() {
|
|
116
155
|
const w = this.width;
|
|
117
156
|
const h = this.height;
|
|
@@ -121,6 +160,7 @@ export class CocoaPaneWindow {
|
|
|
121
160
|
this._surfaceSize?.height !== h
|
|
122
161
|
) {
|
|
123
162
|
const hadSurface = Boolean(this._ring);
|
|
163
|
+
this._releaseRing();
|
|
124
164
|
this._ring = [];
|
|
125
165
|
for (let i = 0; i < CocoaPaneWindow.RING; i += 1) {
|
|
126
166
|
const s = this._native.createSurfaceIOSurface(w, h, this.scale, true);
|
|
@@ -134,12 +174,9 @@ export class CocoaPaneWindow {
|
|
|
134
174
|
this._surfaceSize = { width: w, height: h };
|
|
135
175
|
this._surfaceGen++;
|
|
136
176
|
this._flushDamage = 'full';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (node && !node.destroyed) node.invalidate(true, null, 'resize');
|
|
141
|
-
});
|
|
142
|
-
}
|
|
177
|
+
// decided when the flush reports its rects — see CocoaWindow's
|
|
178
|
+
// `_ensureSurface` for why not a queued full frame from here
|
|
179
|
+
if (hadSurface) this._freshSurface = true;
|
|
143
180
|
}
|
|
144
181
|
return this._surface;
|
|
145
182
|
}
|
|
@@ -163,6 +200,20 @@ export class CocoaPaneWindow {
|
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
noteFrameDamage(rects) {
|
|
203
|
+
if (this._freshSurface) {
|
|
204
|
+
this._freshSurface = false;
|
|
205
|
+
// a bounded flush onto a fresh ring leaves garbage outside its rects:
|
|
206
|
+
// one full frame, and no present until it lands (CocoaWindow's rule)
|
|
207
|
+
if (rects) {
|
|
208
|
+
this._holdPresent = true;
|
|
209
|
+
const node = this._reactX11Node;
|
|
210
|
+
if (node && !node.destroyed) node.invalidate(false, null, 'resize');
|
|
211
|
+
} else {
|
|
212
|
+
this._holdPresent = false;
|
|
213
|
+
}
|
|
214
|
+
} else if (!rects) {
|
|
215
|
+
this._holdPresent = false;
|
|
216
|
+
}
|
|
166
217
|
if (this._flushDamage === 'full') return;
|
|
167
218
|
if (!rects) {
|
|
168
219
|
this._flushDamage = 'full';
|
|
@@ -190,6 +241,7 @@ export class CocoaPaneWindow {
|
|
|
190
241
|
/** Flip and tell the host, instead of touching any layer of our own. */
|
|
191
242
|
present() {
|
|
192
243
|
if (!this._dirty || !this._ring || this.destroyed) return;
|
|
244
|
+
if (this._holdPresent) return;
|
|
193
245
|
this._dirty = false;
|
|
194
246
|
const shown = this._ring[this._drawIndex];
|
|
195
247
|
this._native.surfaceUnlock(shown.handle);
|
|
@@ -202,6 +254,7 @@ export class CocoaPaneWindow {
|
|
|
202
254
|
width: this.width,
|
|
203
255
|
height: this.height,
|
|
204
256
|
});
|
|
257
|
+
this._presentedAt = performance.now();
|
|
205
258
|
this._shownIndex = this._drawIndex;
|
|
206
259
|
// the next buffer round the ring — two behind what the host will be
|
|
207
260
|
// showing, so it is safe to write even before the host has switched
|
|
@@ -230,7 +283,6 @@ export class CocoaPaneWindow {
|
|
|
230
283
|
if (this.destroyed) return;
|
|
231
284
|
this.destroyed = true;
|
|
232
285
|
this.app._unregisterWindow(this);
|
|
233
|
-
this.
|
|
234
|
-
this._surface = null;
|
|
286
|
+
this._releaseRing();
|
|
235
287
|
}
|
|
236
288
|
}
|