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.
- package/README.md +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +292 -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 +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -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/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- 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 +334 -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 +945 -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 +11 -1
- 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
package/src/cocoa/app.js
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
// createWindow / fonts / clipboard / X-stub / close, plus the event pump
|
|
5
5
|
// that stands in for the X socket.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// and the run loop", option 1):
|
|
9
|
-
// AppKit
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the
|
|
14
|
-
//
|
|
7
|
+
// Two run loops. By default Node's loop is master and a timer pumps AppKit
|
|
8
|
+
// (docs/macos.md §"Input and the run loop", option 1): input waits for the
|
|
9
|
+
// pump, and AppKit's modal loops (live resize, menu tracking, a drag) stall
|
|
10
|
+
// JS timers — but not the delegate callbacks, which is why live resize
|
|
11
|
+
// still relayouts: the resize event flushes the frame synchronously on its
|
|
12
|
+
// way through (`flushPendingFrames`, the same early-flush a click gets).
|
|
13
|
+
// Under the launcher (`node --import react-x11/cocoa-main`, src/cocoa/
|
|
14
|
+
// main.js) the app runs on a worker instead, AppKit keeps the main thread
|
|
15
|
+
// in a real `[NSApp run]`, and this object is fed by the bridge's batches
|
|
16
|
+
// (`_routeBatch`) with no pump at all (docs/macos.md §"JS on a worker").
|
|
17
|
+
import { isMainThread } from 'node:worker_threads';
|
|
18
|
+
|
|
15
19
|
import { cssColorStraight } from 'ntk';
|
|
16
20
|
|
|
17
21
|
import { deliverActivate, deliverOpen } from '../application.js';
|
|
@@ -37,6 +41,7 @@ import { CocoaSurface } from './surface.js';
|
|
|
37
41
|
import { CocoaWindow } from './window.js';
|
|
38
42
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
39
43
|
import { loadNative } from './native.js';
|
|
44
|
+
import { requestAppKit, threadedChannel } from './threaded.js';
|
|
40
45
|
|
|
41
46
|
// The frame interval: how often a scheduled frame may paint, in ms. The
|
|
42
47
|
// default is the period of the display the window is on — `listScreens`
|
|
@@ -50,12 +55,25 @@ const PUMP_INTERVAL_MS = 8;
|
|
|
50
55
|
// How early a pump tick may take a frame that is not quite due, in ms — the
|
|
51
56
|
// drift of a timer, not a fraction of the pump (`_frameDue`).
|
|
52
57
|
const FRAME_SLACK_MS = 1;
|
|
58
|
+
// Threaded mode's live-resize handshake (windowkit/appkit#53): how long
|
|
59
|
+
// AppKit may hold a resize tick for a frame painted at the new size, in ms.
|
|
60
|
+
// The edge moves when the delegate returns, so the budget is how long a
|
|
61
|
+
// slow frame may hold the drag back. 50 covers what a large tree costs —
|
|
62
|
+
// the Cocoa `resize` cell measures 28ms a tick at 3,662 nodes
|
|
63
|
+
// (docs/macos.md) — with room, where the bridge's own test met every tick
|
|
64
|
+
// in 3.1ms against 3ms of layout. Past three 60Hz frames an edge that waits
|
|
65
|
+
// reads as the drag stuttering, so a tree slower than that lets the edge
|
|
66
|
+
// go first and its frame land after (Flutter's Windows embedder waits up
|
|
67
|
+
// to 100, docs/windows.md §"Resize"). `createRoot({ cocoa: { resizeWait } })`
|
|
68
|
+
// sets it; 0 is no handshake at all.
|
|
69
|
+
const RESIZE_WAIT_MS = 50;
|
|
53
70
|
|
|
54
71
|
export class CocoaApp {
|
|
55
72
|
constructor(native, options = {}) {
|
|
56
73
|
this._native = native;
|
|
57
74
|
this.options = options;
|
|
58
|
-
|
|
75
|
+
// CocoaWindow._key -> CocoaWindow: the number, or on a worker the handle
|
|
76
|
+
this._windows = new Map();
|
|
59
77
|
this._grabWindow = null;
|
|
60
78
|
this._rafQueue = []; // [{ cb, wnd }]
|
|
61
79
|
// the app's own frame clock, for a frame no window owns (a pane's)
|
|
@@ -83,6 +101,22 @@ export class CocoaApp {
|
|
|
83
101
|
this._activeDrag = null;
|
|
84
102
|
// set by a quit request; read by close() to end the process
|
|
85
103
|
this._quitting = false;
|
|
104
|
+
// Threaded mode (`start({ channel })`): fed by the bridge's batches
|
|
105
|
+
// instead of a pump. `_batching` is set while one is routed, and what
|
|
106
|
+
// its inputs owe is paid once, at its end (`_routeBatch`).
|
|
107
|
+
this._threaded = false;
|
|
108
|
+
this._batching = false;
|
|
109
|
+
this._inputOwed = false;
|
|
110
|
+
this._wheeled = null;
|
|
111
|
+
this._unsubscribe = null;
|
|
112
|
+
const resizeWait = options.cocoa?.resizeWait ?? RESIZE_WAIT_MS;
|
|
113
|
+
if (!(resizeWait >= 0) || !Number.isFinite(resizeWait)) {
|
|
114
|
+
throw new TypeError(
|
|
115
|
+
'react-x11: cocoa.resizeWait is a number of milliseconds, 0 for ' +
|
|
116
|
+
`none — got ${resizeWait}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
this._resizeWait = resizeWait;
|
|
86
120
|
|
|
87
121
|
// The activation policy has to be fixed before the app finishes
|
|
88
122
|
// launching — a Regular launch registers a Dock tile, so an agent app
|
|
@@ -92,6 +126,27 @@ export class CocoaApp {
|
|
|
92
126
|
// only when the root asked for one: a bridge without the option, or a
|
|
93
127
|
// fake, is left alone.
|
|
94
128
|
const policy = options.cocoa?.activationPolicy;
|
|
129
|
+
// Under the launcher (src/cocoa/main.js) the app launched before this
|
|
130
|
+
// code ran, so the policy is a switch after the fact, and a Regular
|
|
131
|
+
// launch has already registered its Dock tile. The bridge publishes
|
|
132
|
+
// what the app launched as, and takes the launch policy from
|
|
133
|
+
// APPKIT_ACTIVATION_POLICY (windowkit/appkit#67): a mismatch is said,
|
|
134
|
+
// with the variable that fixes it.
|
|
135
|
+
if (policy != null && threadedChannel()) {
|
|
136
|
+
const launched = native.activationPolicy?.();
|
|
137
|
+
if (
|
|
138
|
+
launched &&
|
|
139
|
+
launched !== policy &&
|
|
140
|
+
process.env.NODE_ENV !== 'production'
|
|
141
|
+
) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`react-x11: cocoa.activationPolicy is '${policy}', but the app ` +
|
|
144
|
+
`launched as '${launched}' — under react-x11/cocoa-main it is up ` +
|
|
145
|
+
'before its code runs. Start it with ' +
|
|
146
|
+
`APPKIT_ACTIVATION_POLICY=${policy} to launch as '${policy}'.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
95
150
|
if (policy != null && typeof native.initApp === 'function') {
|
|
96
151
|
native.initApp({ activationPolicy: policy });
|
|
97
152
|
}
|
|
@@ -159,7 +214,9 @@ export class CocoaApp {
|
|
|
159
214
|
// 'auto'` policy both test for this property, so a backend without it
|
|
160
215
|
// (X11, the headless mock) draws the themed controls with no further
|
|
161
216
|
// branching.
|
|
162
|
-
this.nativeBezels = new BezelStore(native
|
|
217
|
+
this.nativeBezels = new BezelStore(native, {
|
|
218
|
+
answersLater: () => this._threaded,
|
|
219
|
+
});
|
|
163
220
|
|
|
164
221
|
// macOS privacy authorizations (src/cocoa/permissions.js). Present
|
|
165
222
|
// exactly when the bridge has them (>= 0.5), and its presence is the
|
|
@@ -242,8 +299,27 @@ export class CocoaApp {
|
|
|
242
299
|
};
|
|
243
300
|
this._atoms = new Map();
|
|
244
301
|
|
|
302
|
+
// ntk's clipboard shape over `NSPasteboard.general` — which is the one
|
|
303
|
+
// pasteboard, and it is CLIPBOARD's. X's other everyday selection,
|
|
304
|
+
// PRIMARY, is taken by *selecting text at all* and pasted with a middle
|
|
305
|
+
// click, and core takes it on every selection gesture (textselection.js
|
|
306
|
+
// `own()`, the text controls' `_ownSelection`). Sent to the pasteboard,
|
|
307
|
+
// each of those gestures was a Copy: a drag across a paragraph replaced
|
|
308
|
+
// the link the user had just copied in another app.
|
|
309
|
+
//
|
|
310
|
+
// So every other name — PRIMARY, SECONDARY, one of the app's own — is a
|
|
311
|
+
// selection nobody on this desktop can paste from: a write resolves and
|
|
312
|
+
// changes nothing, a read finds no owner, `targets()` is empty, `clear()`
|
|
313
|
+
// leaves the pasteboard alone. Deliberately not a PRIMARY kept in this
|
|
314
|
+
// process: its point is the paste in *another* application, and inside
|
|
315
|
+
// one all it would keep is a middle click that types into a text field,
|
|
316
|
+
// which no Mac text field does. An app that wants select-to-copy anyway
|
|
317
|
+
// — a terminal — writes CLIPBOARD from `onSelectionChange`
|
|
318
|
+
// (docs/clipboard.md, "Two clipboards").
|
|
319
|
+
const isClipboard = (selection = 'CLIPBOARD') => selection === 'CLIPBOARD';
|
|
245
320
|
this.clipboard = {
|
|
246
|
-
write: (data) => {
|
|
321
|
+
write: (data, { selection } = {}) => {
|
|
322
|
+
if (!isClipboard(selection)) return Promise.resolve();
|
|
247
323
|
const text =
|
|
248
324
|
typeof data === 'string'
|
|
249
325
|
? data
|
|
@@ -251,28 +327,39 @@ export class CocoaApp {
|
|
|
251
327
|
native.pasteboardWriteText(String(text));
|
|
252
328
|
return Promise.resolve();
|
|
253
329
|
},
|
|
254
|
-
clear: () => {
|
|
255
|
-
native.pasteboardClear();
|
|
330
|
+
clear: (selection) => {
|
|
331
|
+
if (isClipboard(selection)) native.pasteboardClear();
|
|
256
332
|
return Promise.resolve();
|
|
257
333
|
},
|
|
258
|
-
targets: () => {
|
|
259
|
-
|
|
260
|
-
return
|
|
334
|
+
targets: ({ selection } = {}) => {
|
|
335
|
+
if (!isClipboard(selection)) return Promise.resolve([]);
|
|
336
|
+
return this._ask('pasteboardReadText').then((text) =>
|
|
337
|
+
text == null ? [] : ['UTF8_STRING', 'STRING'],
|
|
338
|
+
);
|
|
261
339
|
},
|
|
262
|
-
read: ({ target } = {}) => {
|
|
263
|
-
|
|
264
|
-
if (text == null) {
|
|
340
|
+
read: ({ selection, target } = {}) => {
|
|
341
|
+
if (!isClipboard(selection)) {
|
|
265
342
|
return Promise.reject(
|
|
266
|
-
new Error(
|
|
343
|
+
new Error(
|
|
344
|
+
`clipboard: nothing to paste — ${selection} is an X11 ` +
|
|
345
|
+
'selection, and on macOS only CLIPBOARD is the pasteboard',
|
|
346
|
+
),
|
|
267
347
|
);
|
|
268
348
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
349
|
+
return this._ask('pasteboardReadText').then((text) => {
|
|
350
|
+
if (text == null) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
'clipboard: nothing to paste — the pasteboard is empty',
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (target === undefined) return text;
|
|
356
|
+
if (target === 'UTF8_STRING' || target === 'STRING') {
|
|
357
|
+
return Buffer.from(text, 'utf8');
|
|
358
|
+
}
|
|
359
|
+
throw new Error(
|
|
360
|
+
`clipboard: cannot convert the pasteboard to ${target}`,
|
|
361
|
+
);
|
|
362
|
+
});
|
|
276
363
|
},
|
|
277
364
|
watch: () => Promise.resolve(() => {}),
|
|
278
365
|
};
|
|
@@ -282,6 +369,18 @@ export class CocoaApp {
|
|
|
282
369
|
return typeof value === 'string' ? cssColorStraight(value) : null;
|
|
283
370
|
}
|
|
284
371
|
|
|
372
|
+
/**
|
|
373
|
+
* A question only AppKit can answer, as a promise: asked in the call on
|
|
374
|
+
* the main thread, and through a callback from a worker — the bridge's
|
|
375
|
+
* rule for every read that has to ask AppKit rather than read what it
|
|
376
|
+
* published (windowkit/appkit#51).
|
|
377
|
+
*/
|
|
378
|
+
_ask(verb, ...args) {
|
|
379
|
+
const native = this._native;
|
|
380
|
+
if (!this._threaded) return Promise.resolve(native[verb](...args));
|
|
381
|
+
return new Promise((resolve) => native[verb](...args, resolve));
|
|
382
|
+
}
|
|
383
|
+
|
|
285
384
|
findArgbVisual() {
|
|
286
385
|
// every Cocoa window composites; the "visual" is a formality
|
|
287
386
|
return { visual: 1, depth: 32 };
|
|
@@ -375,7 +474,7 @@ export class CocoaApp {
|
|
|
375
474
|
}
|
|
376
475
|
|
|
377
476
|
_registerWindow(wnd) {
|
|
378
|
-
this._windows.set(wnd.
|
|
477
|
+
this._windows.set(wnd._key, wnd);
|
|
379
478
|
}
|
|
380
479
|
|
|
381
480
|
/**
|
|
@@ -516,14 +615,14 @@ export class CocoaApp {
|
|
|
516
615
|
}
|
|
517
616
|
|
|
518
617
|
_unregisterWindow(wnd) {
|
|
519
|
-
this._windows.delete(wnd.
|
|
618
|
+
this._windows.delete(wnd._key);
|
|
520
619
|
if (this._grabWindow === wnd) this._grabWindow = null;
|
|
521
620
|
}
|
|
522
621
|
|
|
523
622
|
// --- the pump ------------------------------------------------------------
|
|
524
623
|
|
|
525
|
-
start({ pumpInterval = PUMP_INTERVAL_MS } = {}) {
|
|
526
|
-
if (this._pump) return;
|
|
624
|
+
start({ pumpInterval = PUMP_INTERVAL_MS, channel = null } = {}) {
|
|
625
|
+
if (this._pump || this._threaded) return;
|
|
527
626
|
this._pumpInterval = pumpInterval;
|
|
528
627
|
const native = this._native;
|
|
529
628
|
// A pane process has no NSApplication to pump — no windows, no events,
|
|
@@ -535,10 +634,13 @@ export class CocoaApp {
|
|
|
535
634
|
}, pumpInterval);
|
|
536
635
|
return;
|
|
537
636
|
}
|
|
637
|
+
if (channel) {
|
|
638
|
+
this._startThreaded(channel);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
538
641
|
native.initApp();
|
|
539
642
|
native.setBackendEventCallback((ev) => this._route(ev));
|
|
540
643
|
this._pump = setInterval(() => {
|
|
541
|
-
this._endLiveResizes();
|
|
542
644
|
native.pump2(); // flushes the previous tick's CATransaction
|
|
543
645
|
if (this._shadowStale.size) {
|
|
544
646
|
for (const wnd of this._shadowStale) {
|
|
@@ -552,13 +654,17 @@ export class CocoaApp {
|
|
|
552
654
|
}
|
|
553
655
|
|
|
554
656
|
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
657
|
+
* Threaded mode: the launcher's channel is open (src/cocoa/threaded.js),
|
|
658
|
+
* the main thread is in `[NSApp run]`, and this app is on a worker. There
|
|
659
|
+
* is no pump. Events arrive in the bridge's batches whenever AppKit has
|
|
660
|
+
* them, from inside its modal loops too, and the frame clock arms a timer
|
|
661
|
+
* for every frame it is owed, at whatever distance (`_armFrameTimer`).
|
|
559
662
|
*/
|
|
560
|
-
|
|
561
|
-
|
|
663
|
+
_startThreaded(channel) {
|
|
664
|
+
this._threaded = true;
|
|
665
|
+
this._pumpInterval = Infinity;
|
|
666
|
+
this._native.initApp();
|
|
667
|
+
this._unsubscribe = channel.subscribe((batch) => this._routeBatch(batch));
|
|
562
668
|
}
|
|
563
669
|
|
|
564
670
|
/**
|
|
@@ -576,7 +682,7 @@ export class CocoaApp {
|
|
|
576
682
|
*/
|
|
577
683
|
_requestFrame(cb, wnd = null) {
|
|
578
684
|
this._rafQueue.push({ cb, wnd });
|
|
579
|
-
if (this._pump && this._rafQueue.length === 1) {
|
|
685
|
+
if ((this._pump || this._threaded) && this._rafQueue.length === 1) {
|
|
580
686
|
const now = performance.now();
|
|
581
687
|
this._armFrameTimer(Math.max(1, this._frameWait(wnd ?? this, now)), now);
|
|
582
688
|
}
|
|
@@ -632,7 +738,8 @@ export class CocoaApp {
|
|
|
632
738
|
* its own transaction (the bridge flushes on the flip), so what is
|
|
633
739
|
* painted here is on glass without waiting for the next pump. What the
|
|
634
740
|
* tick alone still does is pump AppKit's events, which is what
|
|
635
|
-
* `pumpInterval` stays the cadence of.
|
|
741
|
+
* `pumpInterval` stays the cadence of. With no pump at all — threaded
|
|
742
|
+
* mode, where `_pumpInterval` is Infinity — every frame is one of these.
|
|
636
743
|
*/
|
|
637
744
|
_armFrameTimer(wait, now) {
|
|
638
745
|
if (!(wait > 0 && wait < this._pumpInterval)) return;
|
|
@@ -671,6 +778,15 @@ export class CocoaApp {
|
|
|
671
778
|
this._rafQueue = [];
|
|
672
779
|
let soonest = Infinity;
|
|
673
780
|
for (const entry of queue) {
|
|
781
|
+
// A window whose last flip has not given its back buffer back yet
|
|
782
|
+
// (threaded mode's fence, `CocoaWindow.frameInFlight`) waits before
|
|
783
|
+
// its clock is asked, or the clock would count a frame that did not
|
|
784
|
+
// run. The release is an event, and the batch it arrives in ticks
|
|
785
|
+
// again.
|
|
786
|
+
if (entry.wnd?.frameInFlight?.()) {
|
|
787
|
+
this._rafQueue.push(entry);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
674
790
|
const clock = entry.wnd ?? this;
|
|
675
791
|
if (!due.has(clock)) due.set(clock, this._frameDue(clock, now));
|
|
676
792
|
if (!due.get(clock)) {
|
|
@@ -717,11 +833,51 @@ export class CocoaApp {
|
|
|
717
833
|
* exactly when it was worth showing.
|
|
718
834
|
*/
|
|
719
835
|
_afterInput() {
|
|
836
|
+
// threaded mode pays it once, for the whole batch (`_routeBatch`)
|
|
837
|
+
if (this._batching) {
|
|
838
|
+
this._inputOwed = true;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
720
841
|
flushSyncWork();
|
|
721
842
|
flushPendingFrames();
|
|
722
843
|
this._presentAll();
|
|
723
844
|
}
|
|
724
845
|
|
|
846
|
+
/**
|
|
847
|
+
* Threaded mode's delivery: every event the bridge emitted since the
|
|
848
|
+
* last wake, in order. Each is routed as the pump routes it, but what an
|
|
849
|
+
* input owes — React's half, the paint, the present (`_afterInput`) — is
|
|
850
|
+
* paid once, when the batch is done: ten moves and a click that crossed
|
|
851
|
+
* while this thread was busy are one frame, not eleven. Then what a pump
|
|
852
|
+
* tick does: the frames that are due, and the ones a window's visibility
|
|
853
|
+
* or its back buffer was holding, which an occlusion change or a
|
|
854
|
+
* `surface-released` in this very batch may just have freed.
|
|
855
|
+
*/
|
|
856
|
+
_routeBatch(batch) {
|
|
857
|
+
if (this._closed) return;
|
|
858
|
+
const started = performance.now();
|
|
859
|
+
this._batching = true;
|
|
860
|
+
try {
|
|
861
|
+
for (const ev of batch) this._route(ev);
|
|
862
|
+
} finally {
|
|
863
|
+
this._batching = false;
|
|
864
|
+
}
|
|
865
|
+
if (this._closed) return;
|
|
866
|
+
if (this._inputOwed) {
|
|
867
|
+
this._inputOwed = false;
|
|
868
|
+
this._afterInput();
|
|
869
|
+
}
|
|
870
|
+
// a frame that answered the wheel is this refresh's (`_routeWheel`)
|
|
871
|
+
if (this._wheeled) {
|
|
872
|
+
for (const wnd of this._wheeled) {
|
|
873
|
+
if (wnd._presentedAt >= started) wnd._rafLast = wnd._presentedAt;
|
|
874
|
+
}
|
|
875
|
+
this._wheeled = null;
|
|
876
|
+
}
|
|
877
|
+
this._tickFrames();
|
|
878
|
+
this._presentAll();
|
|
879
|
+
}
|
|
880
|
+
|
|
725
881
|
// --- event routing -------------------------------------------------------
|
|
726
882
|
|
|
727
883
|
_route(ev) {
|
|
@@ -802,15 +958,25 @@ export class CocoaApp {
|
|
|
802
958
|
// id nobody knows is an animation already forgotten (cancelled, or
|
|
803
959
|
// its layer dropped), and the bridge's report is just late
|
|
804
960
|
return this._animationEnds.get(ev.id)?.(ev);
|
|
961
|
+
case 'window-live-resize':
|
|
962
|
+
return this._routeLiveResize(ev);
|
|
963
|
+
case 'window-created':
|
|
964
|
+
// a worker's window, made on the UI thread after the call that
|
|
965
|
+
// answered its handle
|
|
966
|
+
return this._window(ev)?._created(ev);
|
|
967
|
+
case 'surface-released':
|
|
968
|
+
return this._routeSurfaceReleased(ev);
|
|
805
969
|
default:
|
|
806
970
|
return undefined;
|
|
807
971
|
}
|
|
808
972
|
}
|
|
809
973
|
|
|
974
|
+
/** The window an event is about. On a worker every window event names
|
|
975
|
+
* the bridge's handle, which is what windows are keyed by there; in pump
|
|
976
|
+
* mode the events name the number. */
|
|
810
977
|
_window(ev) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
: null;
|
|
978
|
+
const key = ev.handle ?? ev.windowNumber;
|
|
979
|
+
return key != null ? (this._windows.get(key) ?? null) : null;
|
|
814
980
|
}
|
|
815
981
|
|
|
816
982
|
/**
|
|
@@ -894,11 +1060,34 @@ export class CocoaApp {
|
|
|
894
1060
|
});
|
|
895
1061
|
// Painted now, like a press. On X11 the wheel is paced on the frame
|
|
896
1062
|
// clock because ntk coalesces a touchpad's dozens of reports per frame
|
|
897
|
-
// into one event; AppKit
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
1063
|
+
// into one event; AppKit mostly delivers scroll events at the display's
|
|
1064
|
+
// rate, so answering each one is answering once per refresh — and
|
|
1065
|
+
// answering it on the next frame tick instead was a 15ms median between
|
|
1066
|
+
// the notch and the scroll, most of a refresh period of nothing.
|
|
1067
|
+
//
|
|
1068
|
+
// Mostly: a trackpad's momentum lands two in one tick often, and a
|
|
1069
|
+
// second flip inside the refresh draws into the buffer the first one
|
|
1070
|
+
// just took off glass (`CocoaWindow._flippedRecently` says why that
|
|
1071
|
+
// shows). So the rest of a burst lands React's half and leaves the
|
|
1072
|
+
// paint to the paced frame the scroll already asked for — the model
|
|
1073
|
+
// has scrolled, and the next refresh shows all of it.
|
|
1074
|
+
if (wnd._flippedRecently()) {
|
|
1075
|
+
flushSyncWork();
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
// in a batch the frame goes out when the batch is done, and the clock
|
|
1079
|
+
// restarts from it there (`_routeBatch`)
|
|
1080
|
+
if (this._batching) {
|
|
1081
|
+
(this._wheeled ??= new Set()).add(wnd);
|
|
1082
|
+
this._inputOwed = true;
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const started = performance.now();
|
|
901
1086
|
this._afterInput();
|
|
1087
|
+
// …and a frame that answered the wheel is this refresh's frame: the
|
|
1088
|
+
// window's clock restarts from it, or a pump tick a few milliseconds
|
|
1089
|
+
// later finds the clock due and flips again.
|
|
1090
|
+
if (wnd._presentedAt >= started) wnd._rafLast = wnd._presentedAt;
|
|
902
1091
|
}
|
|
903
1092
|
|
|
904
1093
|
_routeKey(ev) {
|
|
@@ -934,6 +1123,10 @@ export class CocoaApp {
|
|
|
934
1123
|
// During a live resize AppKit's modal loop owns the thread and Node
|
|
935
1124
|
// timers stall; flushing here is what keeps layout tracking the drag.
|
|
936
1125
|
this._afterInput();
|
|
1126
|
+
// On a worker no modal loop holds this thread: a microtask queued here
|
|
1127
|
+
// runs as soon as the batch returns, and what it commits schedules its
|
|
1128
|
+
// frame like any other.
|
|
1129
|
+
if (this._threaded) return;
|
|
937
1130
|
// The React HALF of the response — an anchored popup following the
|
|
938
1131
|
// window, an onResize setState — commits on a microtask AFTER this
|
|
939
1132
|
// handler returns, and the pump that would paint it is the thing the
|
|
@@ -959,6 +1152,7 @@ export class CocoaApp {
|
|
|
959
1152
|
* present waits with them (`CocoaWindow._visible`); on, the next pump
|
|
960
1153
|
* tick runs the catch-up frame and puts it on glass — no early flush,
|
|
961
1154
|
* since nothing was asked for and the pump is at most one interval away.
|
|
1155
|
+
* On a worker the end of the batch the event came in is that tick.
|
|
962
1156
|
*/
|
|
963
1157
|
_routeOcclusion(ev) {
|
|
964
1158
|
const wnd = this._window(ev);
|
|
@@ -966,6 +1160,33 @@ export class CocoaApp {
|
|
|
966
1160
|
wnd._occluded = ev.visible === false;
|
|
967
1161
|
}
|
|
968
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* `windowWillStartLiveResize:` / `windowDidEndLiveResize:`, as the bridge
|
|
1165
|
+
* brackets the drag AppKit's tracking loop runs (windowkit/appkit#63).
|
|
1166
|
+
* Between the two a tick lays out with the floors it has; the end is the
|
|
1167
|
+
* catch-up frame's cue (nodes/window/size.js), which the next frame tick
|
|
1168
|
+
* runs — in pump mode the tick after the loop lets go, on a worker the
|
|
1169
|
+
* end of this batch.
|
|
1170
|
+
*/
|
|
1171
|
+
_routeLiveResize(ev) {
|
|
1172
|
+
const wnd = this._window(ev);
|
|
1173
|
+
if (!wnd || wnd.destroyed) return;
|
|
1174
|
+
wnd.liveResizing = ev.phase === 'begin';
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* `surface-released`: a worker's frame took an IOSurface off a layer and
|
|
1179
|
+
* the frame that replaced it has committed (windowkit/appkit#52). The
|
|
1180
|
+
* window whose back buffer that was stops waiting on it
|
|
1181
|
+
* (`CocoaWindow._surfaceReleased`), and the tick at the end of this
|
|
1182
|
+
* batch runs the frame it was holding.
|
|
1183
|
+
*/
|
|
1184
|
+
_routeSurfaceReleased(ev) {
|
|
1185
|
+
for (const wnd of this._windows.values()) {
|
|
1186
|
+
if (wnd._surfaceReleased(ev.id)) return;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
969
1190
|
_routeAccessibility(ev) {
|
|
970
1191
|
for (const fn of [...this._a11yListeners]) fn(ev);
|
|
971
1192
|
}
|
|
@@ -1126,7 +1347,9 @@ export class CocoaApp {
|
|
|
1126
1347
|
this._frameTimer = null;
|
|
1127
1348
|
this._cocoaGL?.destroy();
|
|
1128
1349
|
this._cocoaGL = null;
|
|
1129
|
-
this.
|
|
1350
|
+
this._unsubscribe?.();
|
|
1351
|
+
this._unsubscribe = null;
|
|
1352
|
+
if (!this._threaded) this._native.setBackendEventCallback(null);
|
|
1130
1353
|
for (const wnd of [...this._windows.values()]) wnd.destroy();
|
|
1131
1354
|
// A quit the app accepted: the tree is down and the connection closed,
|
|
1132
1355
|
// and macOS is still waiting on the Cancel the bridge answered with. End
|
|
@@ -1187,12 +1410,32 @@ export function screenLayout(screens, scale) {
|
|
|
1187
1410
|
*/
|
|
1188
1411
|
export async function createCocoaApp(options = {}) {
|
|
1189
1412
|
const native = loadNative();
|
|
1413
|
+
// The launcher's channel, when the app is on its worker. A worker without
|
|
1414
|
+
// it has no thread running AppKit: the pump cannot run off the main
|
|
1415
|
+
// thread (the bridge refuses), and a window asked for would never be made.
|
|
1416
|
+
const channel = threadedChannel();
|
|
1417
|
+
if (!channel && !isMainThread) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
'react-x11: the cocoa backend runs on a worker thread only under its ' +
|
|
1420
|
+
'launcher, which keeps AppKit on the main thread — start the app ' +
|
|
1421
|
+
'with `node --import react-x11/cocoa-main app.js`, or create the ' +
|
|
1422
|
+
'root on the main thread.',
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
// AppKit is launched on the first cocoa root, not before — an app on a
|
|
1426
|
+
// worker that never makes one gets no Dock tile (src/cocoa/relaunch.js)
|
|
1427
|
+
if (channel) await requestAppKit(native);
|
|
1190
1428
|
const app = new CocoaApp(native, options);
|
|
1191
1429
|
|
|
1192
1430
|
setScaleForTests(app, app.scale, 'cocoa');
|
|
1193
1431
|
setScreensForTests(app, screenLayout(app._screens, app.scale));
|
|
1194
1432
|
setCompositingForTests(app, true);
|
|
1195
1433
|
|
|
1196
|
-
app.start(options.cocoa
|
|
1434
|
+
app.start({ ...options.cocoa, channel });
|
|
1435
|
+
// On a worker a control's bezel is measured and drawn on the UI thread
|
|
1436
|
+
// and answered later (windowkit/appkit#54), so the metrics layout reads
|
|
1437
|
+
// synchronously are asked for now, beside the rest of startup — the yoga
|
|
1438
|
+
// load `createRoot` runs alongside takes longer.
|
|
1439
|
+
if (channel) await app.nativeBezels.prefetch(app.scale);
|
|
1197
1440
|
return app;
|
|
1198
1441
|
}
|