react-x11 2.16.1 → 2.17.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/README.md +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +17 -9
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/glarea.js +48 -10
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +23 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +21 -7
- package/src/idle.js +59 -1
- package/src/index.d.ts +41 -0
- package/src/index.js +30 -3
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +16 -0
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +66 -0
package/src/cocoa/app.js
CHANGED
|
@@ -38,10 +38,10 @@ import { CocoaPaneWindow } from './panewindow.js';
|
|
|
38
38
|
import { CocoaColorSampler } from './screencolor.js';
|
|
39
39
|
import { CocoaFilePanels } from './filepanels.js';
|
|
40
40
|
import { CocoaFontManager } from './fonts.js';
|
|
41
|
-
import { releaseImageUpload } from '
|
|
41
|
+
import { releaseImageUpload } from '../backend/context2d.js';
|
|
42
42
|
import { CocoaSurface } from './surface.js';
|
|
43
43
|
import { CocoaSymbols } from './symbols.js';
|
|
44
|
-
import { CocoaWindow } from './window.js';
|
|
44
|
+
import { CocoaWindow, FRAME_SLACK_MS } from './window.js';
|
|
45
45
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
46
46
|
import { loadNative } from './native.js';
|
|
47
47
|
import { requestAppKit, threadedChannel } from './threaded.js';
|
|
@@ -55,9 +55,6 @@ import { requestAppKit, threadedChannel } from './threaded.js';
|
|
|
55
55
|
// bridge that does not report it: a 60Hz floor every Mac clears.
|
|
56
56
|
const RAF_INTERVAL_MS = 16;
|
|
57
57
|
const PUMP_INTERVAL_MS = 8;
|
|
58
|
-
// How early a pump tick may take a frame that is not quite due, in ms — the
|
|
59
|
-
// drift of a timer, not a fraction of the pump (`_frameDue`).
|
|
60
|
-
const FRAME_SLACK_MS = 1;
|
|
61
58
|
// Threaded mode's live-resize handshake (windowkit/appkit#53): how long
|
|
62
59
|
// AppKit may hold a resize tick for a frame painted at the new size, in ms.
|
|
63
60
|
// The edge moves when the delegate returns, so the budget is how long a
|
|
@@ -204,7 +201,7 @@ export class CocoaApp {
|
|
|
204
201
|
native.setAppName(String(appName));
|
|
205
202
|
}
|
|
206
203
|
|
|
207
|
-
// The Dock menu (src/cocoa/dock.js), installed by `
|
|
204
|
+
// The Dock menu (src/cocoa/dock.js), installed by `useLauncherMenu`.
|
|
208
205
|
this._dockMenu = new CocoaDockMenu(this);
|
|
209
206
|
// The tray items (src/cocoa/statusitem.js), by the bridge's handle —
|
|
210
207
|
// which is what a click event names them by.
|
|
@@ -703,8 +700,12 @@ export class CocoaApp {
|
|
|
703
700
|
this._native.setDockBadge(label == null ? null : String(label));
|
|
704
701
|
}
|
|
705
702
|
|
|
706
|
-
/**
|
|
707
|
-
|
|
703
|
+
/**
|
|
704
|
+
* The menu behind a right-click on the launcher icon — `useLauncherMenu()`.
|
|
705
|
+
* Here the launcher is the Dock, which is why the AppKit call below keeps
|
|
706
|
+
* its own name: `_native.setDockMenu` *is* what Apple calls it.
|
|
707
|
+
*/
|
|
708
|
+
setLauncherMenu(items) {
|
|
708
709
|
this._dockMenu.update(items);
|
|
709
710
|
}
|
|
710
711
|
|
|
@@ -902,6 +903,13 @@ export class CocoaApp {
|
|
|
902
903
|
* tick alone still does is pump AppKit's events, which is what
|
|
903
904
|
* `pumpInterval` stays the cadence of. With no pump at all — threaded
|
|
904
905
|
* mode, where `_pumpInterval` is Infinity — every frame is one of these.
|
|
906
|
+
*
|
|
907
|
+
* The wait is rounded **up** to whole milliseconds, because `setTimeout`
|
|
908
|
+
* only counts those: a 120Hz panel's 8.333ms gate rounded to 8 fired
|
|
909
|
+
* before the clock would take the frame, which cost a second timer per
|
|
910
|
+
* frame to cover the last third of a millisecond. Rounding up costs the
|
|
911
|
+
* grid nothing — `_frameDue` anchors the next slot to the last one, not
|
|
912
|
+
* to when the frame ran — and lands on the first tick the clock accepts.
|
|
905
913
|
*/
|
|
906
914
|
_armFrameTimer(wait, now) {
|
|
907
915
|
if (!(wait > 0 && wait < this._pumpInterval)) return;
|
|
@@ -918,7 +926,7 @@ export class CocoaApp {
|
|
|
918
926
|
this._tickFrames();
|
|
919
927
|
this._presentAll();
|
|
920
928
|
},
|
|
921
|
-
Math.max(1, Math.
|
|
929
|
+
Math.max(1, Math.ceil(wait)),
|
|
922
930
|
);
|
|
923
931
|
}
|
|
924
932
|
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -846,7 +846,7 @@ export class CocoaFontManager {
|
|
|
846
846
|
}
|
|
847
847
|
|
|
848
848
|
/**
|
|
849
|
-
* The CTFont a glyph run draws with (`
|
|
849
|
+
* The CTFont a glyph run draws with (`BackendContext2D.drawGlyphs`): a face
|
|
850
850
|
* of this engine's at the run's size, or an ntk `Font` — `openFont()`'s —
|
|
851
851
|
* resolved to CoreText from the same bytes, so the glyph ids it shaped
|
|
852
852
|
* with hold. Null for anything else, and the run is skipped.
|
package/src/cocoa/glarea.js
CHANGED
|
@@ -231,6 +231,16 @@ export class CocoaGLArea {
|
|
|
231
231
|
return this.parent.requestAnimationFrame(cb);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The owning window's clock, asked where a gate of `interval` ms lands
|
|
236
|
+
* on its grid (`CocoaWindow.nextFrameAt`) — the swap gate phase-locks to
|
|
237
|
+
* it so the two do not stack. A parent with no clock (a bare stub in a
|
|
238
|
+
* test) answers one interval from now, the old swap-relative wait.
|
|
239
|
+
*/
|
|
240
|
+
nextFrameAt(interval) {
|
|
241
|
+
return this.parent.nextFrameAt?.(interval) ?? performance.now() + interval;
|
|
242
|
+
}
|
|
243
|
+
|
|
234
244
|
getContext(kind, config) {
|
|
235
245
|
if (kind !== 'opengl' || this.destroyed) return null;
|
|
236
246
|
if (!this._context) {
|
|
@@ -262,7 +272,10 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
262
272
|
let back = null;
|
|
263
273
|
let width = 0;
|
|
264
274
|
let height = 0;
|
|
265
|
-
|
|
275
|
+
// The moment the swap gate reopens, on the window clock's grid — not a
|
|
276
|
+
// boolean a timer flips, so a frame that arrives a fraction of a
|
|
277
|
+
// millisecond before its own timer still reads the true state.
|
|
278
|
+
let gateOpenAt = -Infinity;
|
|
266
279
|
let gateTimer = null;
|
|
267
280
|
let destroyed = false;
|
|
268
281
|
|
|
@@ -289,7 +302,17 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
289
302
|
// one, so the honest gate is one display period per swap — the same
|
|
290
303
|
// timer-reopened gate the XQuartz CGL flavor uses (no backpressure
|
|
291
304
|
// exists on either).
|
|
292
|
-
|
|
305
|
+
//
|
|
306
|
+
// One period *per frame*, though, not per swap: timed from the swap the
|
|
307
|
+
// gate reopened at `slot + drawCost + period`, which overshoots the
|
|
308
|
+
// window clock's next slot however cheap the frame, and the clock then
|
|
309
|
+
// rounded it up to the slot after — two display periods, a 120Hz panel
|
|
310
|
+
// pinned at 60fps whatever the scene cost (issue #631). Anchored to the
|
|
311
|
+
// slot the frame was due at (`CocoaWindow.nextFrameAt`) the two gates
|
|
312
|
+
// are on one grid: the clock cannot take a frame the gate would refuse,
|
|
313
|
+
// because both are the same instant, and the gate still never lets two
|
|
314
|
+
// frames into one period.
|
|
315
|
+
ctx.canRender = (now = performance.now()) => !destroyed && now >= gateOpenAt;
|
|
293
316
|
|
|
294
317
|
ctx.makeCurrent = () => {
|
|
295
318
|
if (destroyed) return;
|
|
@@ -305,6 +328,26 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
305
328
|
runtime.gl.bindFramebuffer(target, fb == null ? (back?.fbo ?? 0) : fb);
|
|
306
329
|
};
|
|
307
330
|
|
|
331
|
+
// A frame refused by the gate has nothing else to wake it, so the gate
|
|
332
|
+
// says when it opens. `setTimeout` counts whole milliseconds and a
|
|
333
|
+
// display period is rarely one (8.333 at 120Hz), so a timer that lands
|
|
334
|
+
// early re-arms rather than announcing a gate that is still shut.
|
|
335
|
+
const armGate = () => {
|
|
336
|
+
if (gateTimer || destroyed) return;
|
|
337
|
+
const wait = gateOpenAt - performance.now();
|
|
338
|
+
gateTimer = setTimeout(
|
|
339
|
+
() => {
|
|
340
|
+
gateTimer = null;
|
|
341
|
+
if (destroyed) return;
|
|
342
|
+
if (performance.now() < gateOpenAt) armGate();
|
|
343
|
+
else ctx.onFrameAvailable?.();
|
|
344
|
+
},
|
|
345
|
+
Math.max(1, Math.ceil(wait)),
|
|
346
|
+
);
|
|
347
|
+
// the timer must not hold the process open for an idle scene
|
|
348
|
+
gateTimer.unref?.();
|
|
349
|
+
};
|
|
350
|
+
|
|
308
351
|
ctx.SwapBuffers = () => {
|
|
309
352
|
if (destroyed || !back) return;
|
|
310
353
|
runtime.gl.flush();
|
|
@@ -312,14 +355,9 @@ function createGLAreaContext(runtime, area, config) {
|
|
|
312
355
|
const shown = back;
|
|
313
356
|
back = front;
|
|
314
357
|
front = shown;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
gateClosed = false;
|
|
319
|
-
ctx.onFrameAvailable?.();
|
|
320
|
-
}, runtime.frameInterval);
|
|
321
|
-
// the timer must not hold the process open for an idle scene
|
|
322
|
-
gateTimer.unref?.();
|
|
358
|
+
// one display period from the frame's slot, not from this moment
|
|
359
|
+
gateOpenAt = area.nextFrameAt(runtime.frameInterval);
|
|
360
|
+
armGate();
|
|
323
361
|
};
|
|
324
362
|
|
|
325
363
|
ctx._resized = () => {
|
package/src/cocoa/overlay.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// `map`, `unmap`, `getContext`, `destroy` — plus `present`, which puts what
|
|
15
15
|
// was painted on the layer: ntk blits an X window's backing store on its
|
|
16
16
|
// own, where a layer's contents are a copy the bitmap has to be pushed to.
|
|
17
|
-
import {
|
|
17
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
18
|
|
|
19
19
|
export const OVERLAY_Z = 1e7 + 1;
|
|
20
20
|
|
|
@@ -115,7 +115,7 @@ export class CocoaOverlayPane {
|
|
|
115
115
|
|
|
116
116
|
getContext() {
|
|
117
117
|
if (!this._ctx) {
|
|
118
|
-
this._ctx = new
|
|
118
|
+
this._ctx = new BackendContext2D(
|
|
119
119
|
this._native,
|
|
120
120
|
() => this._ensureSurface(),
|
|
121
121
|
() => {
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// via emit. Geometry and input arrive as channel messages (the host owns
|
|
15
15
|
// layout and hit-testing — this is CPU offloading, not isolation), and the
|
|
16
16
|
// only outbound traffic is pane-present.
|
|
17
|
-
import {
|
|
17
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
18
|
|
|
19
19
|
let nextPaneId = 1;
|
|
20
20
|
|
|
@@ -183,7 +183,7 @@ export class CocoaPaneWindow {
|
|
|
183
183
|
|
|
184
184
|
getContext() {
|
|
185
185
|
if (!this._ctx) {
|
|
186
|
-
this._ctx = new
|
|
186
|
+
this._ctx = new BackendContext2D(
|
|
187
187
|
this._native,
|
|
188
188
|
() => this._ensureSurface(),
|
|
189
189
|
() => {
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -30,7 +30,7 @@ import { Node } from '../nodes/node.js';
|
|
|
30
30
|
import { addDamageRect, damageToPaint } from '../nodes/damage.js';
|
|
31
31
|
import { intersectRects } from '../nodes/rects.js';
|
|
32
32
|
import { EASING_CONTROL_POINTS, TRANSITION_CONTROL_POINTS } from '../styles.js';
|
|
33
|
-
import {
|
|
33
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
34
34
|
|
|
35
35
|
export const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
36
36
|
|
|
@@ -431,7 +431,7 @@ export class RasterState {
|
|
|
431
431
|
this.height = height;
|
|
432
432
|
this.gen++;
|
|
433
433
|
if (!this.ctx) {
|
|
434
|
-
this.ctx = new
|
|
434
|
+
this.ctx = new BackendContext2D(
|
|
435
435
|
presenter.native,
|
|
436
436
|
() => this.surface,
|
|
437
437
|
() => this.gen,
|
package/src/cocoa/surface.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// buffer the same way on both backends and names neither:
|
|
7
7
|
//
|
|
8
8
|
// const surface = new Surface(app, { width, height }); // device pixels
|
|
9
|
-
// const ctx = surface.getContext('2d'); // a
|
|
9
|
+
// const ctx = surface.getContext('2d'); // a BackendContext2D
|
|
10
10
|
// ctx.fillRect(0, 0, width, height);
|
|
11
11
|
// surface.copyWithin({ x: 0, y: 0, width, height }, 0, -rowHeight);
|
|
12
12
|
// windowCtx.drawImage(surface, x, y); // one composite
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// one from `contentBox()` numbers, which are device pixels already
|
|
44
44
|
// (docs/scale.md). The bridge is told the app's scale so the bitmap carries
|
|
45
45
|
// it — inert for a `drawImage` source, right for a layer's contents.
|
|
46
|
-
import {
|
|
46
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
47
47
|
|
|
48
48
|
export class CocoaSurface {
|
|
49
49
|
constructor(app, { width, height, format = 'argb32' } = {}) {
|
|
@@ -113,7 +113,7 @@ export class CocoaSurface {
|
|
|
113
113
|
|
|
114
114
|
_context() {
|
|
115
115
|
if (!this._ctx) {
|
|
116
|
-
this._ctx = new
|
|
116
|
+
this._ctx = new BackendContext2D(
|
|
117
117
|
this._native,
|
|
118
118
|
() => this._handle(),
|
|
119
119
|
() => 1,
|
package/src/cocoa/window.js
CHANGED
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
// an X window — attributes, reported width/height, event coordinates,
|
|
7
7
|
// _screenOrigin. The divide-by-scale into Cocoa points happens against the
|
|
8
8
|
// native layer and nowhere above it.
|
|
9
|
-
import {
|
|
9
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
10
10
|
import { CocoaDropTransport, dragSpec } from './dnd.js';
|
|
11
11
|
import { CocoaLayerPresenter } from './presenter.js';
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
13
13
|
|
|
14
14
|
let nextWindowId = 1;
|
|
15
|
+
// The slack a frame clock allows under its interval, in ms: a timer's drift
|
|
16
|
+
// and no more (`CocoaApp._frameDue`, `_frameWait`, `nextFrameAt`). It lives
|
|
17
|
+
// here because the window is the clock — `_rafLast` and `_frameInterval`
|
|
18
|
+
// are its fields — and the app imports it from this side.
|
|
19
|
+
export const FRAME_SLACK_MS = 1;
|
|
15
20
|
// How long a worker's flip may hold its window's next frame (`_armFence`).
|
|
16
21
|
// The release is reported once the replacing frame has committed, a
|
|
17
22
|
// fraction of a millisecond later, and a window must not freeze on a report
|
|
@@ -794,7 +799,7 @@ export class CocoaWindow {
|
|
|
794
799
|
|
|
795
800
|
getContext() {
|
|
796
801
|
if (!this._ctx) {
|
|
797
|
-
this._ctx = new
|
|
802
|
+
this._ctx = new BackendContext2D(
|
|
798
803
|
this._native,
|
|
799
804
|
() => this._ensureSurface(),
|
|
800
805
|
() => {
|
|
@@ -910,6 +915,22 @@ export class CocoaWindow {
|
|
|
910
915
|
return this.app._requestFrame(cb, this);
|
|
911
916
|
}
|
|
912
917
|
|
|
918
|
+
/**
|
|
919
|
+
* The earliest moment this window's clock will hand out another frame,
|
|
920
|
+
* for a gate of `interval` ms — the display's period by default, which
|
|
921
|
+
* is the clock's own.
|
|
922
|
+
*
|
|
923
|
+
* It is the grid `_frameDue` decides on, not the wall clock: the anchor
|
|
924
|
+
* is the slot the running frame was *due* at, so the answer does not
|
|
925
|
+
* move with how long that frame took or how late its timer fired. A
|
|
926
|
+
* second gate that wants to compose with this clock rather than be
|
|
927
|
+
* rounded up by it has to land on this grid — which is what a
|
|
928
|
+
* `<glarea>`'s swap gate reads it for (src/cocoa/glarea.js).
|
|
929
|
+
*/
|
|
930
|
+
nextFrameAt(interval = this._frameInterval) {
|
|
931
|
+
return this._rafLast + interval - FRAME_SLACK_MS;
|
|
932
|
+
}
|
|
933
|
+
|
|
913
934
|
/**
|
|
914
935
|
* Push the backing surface at the WindowServer, if anything drew — and
|
|
915
936
|
* tell the window node what it cost. The flip itself is cheap, and the
|
package/src/events.js
CHANGED
|
@@ -113,6 +113,27 @@ class SyntheticEvent {
|
|
|
113
113
|
this.ctrlKey = Boolean(native?.buttons & MOD.Control);
|
|
114
114
|
this.altKey = Boolean(native?.buttons & MOD.Alt);
|
|
115
115
|
this.metaKey = Boolean(native?.buttons & MOD.Super);
|
|
116
|
+
// Where the pointer is on the **virtual screen**, in the same logical
|
|
117
|
+
// pixels `x`/`y` are in — the DOM's name for the DOM's quantity, and the
|
|
118
|
+
// one an app should reach for when it places something outside the
|
|
119
|
+
// window (a context menu at the pointer, a drag preview).
|
|
120
|
+
//
|
|
121
|
+
// `nativeEvent.rootx`/`rooty` is X11's name for it, is still there, and
|
|
122
|
+
// is still in *device* pixels: the same split as `ev.x` against
|
|
123
|
+
// `nativeEvent.x`. Defined exactly where the backend reported a position
|
|
124
|
+
// — an X11 KeyPress carries one too, so this is not pointer-events-only —
|
|
125
|
+
// and absent otherwise, because a made-up 0 would read as the screen's
|
|
126
|
+
// top-left corner rather than as "no answer".
|
|
127
|
+
if (native?.rootx !== undefined && native?.rootx !== null) {
|
|
128
|
+
// The **window's** scale, not the target's. `x`/`y` are in the target's
|
|
129
|
+
// unit on purpose, so a subtree zoomed by a `scale` prop reads its own
|
|
130
|
+
// — but a screen coordinate is not in that subtree's space at all, and
|
|
131
|
+
// dividing it by a zoom factor would put it somewhere nobody is. The
|
|
132
|
+
// drag events have always computed it this way (src/dnd.js).
|
|
133
|
+
const screen = manager.scale;
|
|
134
|
+
this.screenX = native.rootx / screen;
|
|
135
|
+
this.screenY = (native.rooty ?? 0) / screen;
|
|
136
|
+
}
|
|
116
137
|
this.defaultPrevented = false;
|
|
117
138
|
this.propagationStopped = false;
|
|
118
139
|
if (extra) Object.assign(this, extra);
|
package/src/foreignnodes.js
CHANGED
|
@@ -192,10 +192,15 @@ export class ForeignNode extends Node {
|
|
|
192
192
|
*/
|
|
193
193
|
_refuse() {
|
|
194
194
|
this._refused = true;
|
|
195
|
+
// Named by the capability, not by the backend that happens to have it.
|
|
196
|
+
// An app cannot act on "use X11", and a second backend growing embedding
|
|
197
|
+
// would make that wording wrong as well as unhelpful; what an app *can*
|
|
198
|
+
// act on is the question the last sentence names (AGENTS.md,
|
|
199
|
+
// "Vocabulary").
|
|
195
200
|
const err = new Error(
|
|
196
|
-
'react-x11: <foreign> needs
|
|
197
|
-
'
|
|
198
|
-
"useSupports('embedding') before rendering one.",
|
|
201
|
+
'react-x11: <foreign> needs a backend with cross-process window ' +
|
|
202
|
+
'embedding, and this one has none — so nothing can be put in it. ' +
|
|
203
|
+
"Ask useSupports('embedding') before rendering one.",
|
|
199
204
|
);
|
|
200
205
|
this.error = err;
|
|
201
206
|
// The client is the whole reason this node is a Tab stop by default, and
|
package/src/frame/index.js
CHANGED
|
@@ -44,6 +44,7 @@ import React, {
|
|
|
44
44
|
} from 'react';
|
|
45
45
|
|
|
46
46
|
import { useAppOrNull } from '../appcontext.js';
|
|
47
|
+
import { canEmbed } from '../embedding.js';
|
|
47
48
|
import { FrameEnv } from './env.js';
|
|
48
49
|
import { CallbackTable, PROTOCOL } from './protocol.js';
|
|
49
50
|
|
|
@@ -221,11 +222,14 @@ export function Frame({
|
|
|
221
222
|
ref,
|
|
222
223
|
}) {
|
|
223
224
|
const env = useContext(FrameEnv);
|
|
224
|
-
// A backend that composites panes from shared
|
|
225
|
-
// itself with createPaneHost; everything else embeds the pane's
|
|
226
|
-
// window through <foreign>, exactly as before.
|
|
225
|
+
// A backend that composites panes from a shared buffer (Cocoa, Windows)
|
|
226
|
+
// declares itself with createPaneHost; everything else embeds the pane's
|
|
227
|
+
// real window through <foreign>, exactly as before.
|
|
227
228
|
const appOrNull = useAppOrNull();
|
|
228
229
|
const paneApp = appOrNull?.createPaneHost ? appOrNull : null;
|
|
230
|
+
// Two mechanisms, one question: is there any way to *show* a pane here?
|
|
231
|
+
// Asked before the fork rather than after it — see the session effect.
|
|
232
|
+
const canShowPane = Boolean(paneApp) || canEmbed(appOrNull);
|
|
229
233
|
const [state, setState] = useState({
|
|
230
234
|
phase: 'starting',
|
|
231
235
|
windowId: null,
|
|
@@ -262,6 +266,28 @@ export function Frame({
|
|
|
262
266
|
setState({ phase: 'failed', windowId: null, error });
|
|
263
267
|
};
|
|
264
268
|
|
|
269
|
+
// Before the fork, not after it. A backend with neither mechanism used
|
|
270
|
+
// to start the pane, let it load its module and mount, and only then
|
|
271
|
+
// discover at the embed that there was nowhere to put it — a whole
|
|
272
|
+
// process spawned and killed to reach a conclusion the app object had
|
|
273
|
+
// all along. The fallback it renders is the same one either way; what
|
|
274
|
+
// changes is that it renders at once and costs nothing.
|
|
275
|
+
if (!canShowPane) {
|
|
276
|
+
fail(
|
|
277
|
+
Object.assign(
|
|
278
|
+
new Error(
|
|
279
|
+
'react-x11: <Frame> needs a backend that can show a pane — one ' +
|
|
280
|
+
'that composites panes from a shared buffer, or one with ' +
|
|
281
|
+
'cross-process window embedding — and this one has neither, ' +
|
|
282
|
+
'so no pane was started. Ask ' +
|
|
283
|
+
"useSupports('embedding') before rendering one.",
|
|
284
|
+
),
|
|
285
|
+
{ phase: 'embed' },
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
265
291
|
let t;
|
|
266
292
|
try {
|
|
267
293
|
t = makeTransport({ src: source, display });
|
|
@@ -396,7 +422,7 @@ export function Frame({
|
|
|
396
422
|
// ask, and everything is unref'd so nothing holds the host open
|
|
397
423
|
if (session.current === s) session.current = null;
|
|
398
424
|
};
|
|
399
|
-
}, [source, display, generation, makeTransport]);
|
|
425
|
+
}, [source, display, generation, makeTransport, canShowPane]);
|
|
400
426
|
|
|
401
427
|
// One update per commit that changed the pane's inputs, props and env in
|
|
402
428
|
// the same message — so a theme flip and the state change that caused it
|
package/src/glnodes.js
CHANGED
|
@@ -58,9 +58,20 @@ export function glxConfig(app, spec) {
|
|
|
58
58
|
.chooseGLXConfig(spec)
|
|
59
59
|
.then((config) => ({ backend: 'indirect', ...config }));
|
|
60
60
|
} else {
|
|
61
|
+
// Which backend is asking decides what the honest answer is. Telling a
|
|
62
|
+
// Windows user to upgrade ntk sends them after a package that backend
|
|
63
|
+
// does not use, and an error a developer cannot act on is worse than
|
|
64
|
+
// the feature simply being absent (AGENTS.md, "An error you hit is an
|
|
65
|
+
// error an app developer will hit").
|
|
61
66
|
promise = Promise.reject(
|
|
62
67
|
new Error(
|
|
63
|
-
|
|
68
|
+
process.platform === 'win32'
|
|
69
|
+
? 'react-x11: <glarea> is not built on the win32 backend yet — it ' +
|
|
70
|
+
'needs ANGLE (EGL and GLES over Direct3D 11), which ' +
|
|
71
|
+
'docs/windows.md plans as an optional dependency the way ' +
|
|
72
|
+
'x11-dri is on X11. Everything else on this backend works ' +
|
|
73
|
+
'without it; for GL content today, use the X11 backend.'
|
|
74
|
+
: 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
|
|
64
75
|
),
|
|
65
76
|
);
|
|
66
77
|
}
|
|
@@ -365,18 +376,21 @@ export class GlAreaNode extends Node {
|
|
|
365
376
|
typeof this.window.requestAnimationFrame === 'function'
|
|
366
377
|
? (cb) => this.window.requestAnimationFrame(cb)
|
|
367
378
|
: (cb) => setImmediate(cb);
|
|
368
|
-
|
|
379
|
+
// the frame clock's own timestamp, not a fresh reading: a gate that
|
|
380
|
+
// composes with that clock is answered from the same moment it is
|
|
381
|
+
// (`canRender` on the Cocoa surface, issue #631)
|
|
382
|
+
schedule((now) => {
|
|
369
383
|
this._frameScheduled = false;
|
|
370
|
-
this._drawFrame();
|
|
384
|
+
this._drawFrame(now);
|
|
371
385
|
});
|
|
372
386
|
}
|
|
373
387
|
|
|
374
|
-
_drawFrame() {
|
|
388
|
+
_drawFrame(now) {
|
|
375
389
|
const pacer = this._pacer;
|
|
376
390
|
pacer.began();
|
|
377
391
|
let drawn = false;
|
|
378
392
|
try {
|
|
379
|
-
drawn = this._drawFrameNow();
|
|
393
|
+
drawn = this._drawFrameNow(now);
|
|
380
394
|
} finally {
|
|
381
395
|
pacer.ended(undefined, drawn);
|
|
382
396
|
}
|
|
@@ -386,14 +400,14 @@ export class GlAreaNode extends Node {
|
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
/** The frame itself; true when it drew. */
|
|
389
|
-
_drawFrameNow() {
|
|
403
|
+
_drawFrameNow(now) {
|
|
390
404
|
const gl = this.gl;
|
|
391
405
|
if (!gl || this.destroyed) return false;
|
|
392
406
|
const direct = gl.backend === 'direct';
|
|
393
407
|
// On the direct backend every buffer may still be held by the display,
|
|
394
408
|
// and drawing into one before it comes back would paint what is on
|
|
395
409
|
// screen. `onFrameAvailable` asks for this frame again when one frees.
|
|
396
|
-
if (direct && gl.canRender && !gl.canRender()) {
|
|
410
|
+
if (direct && gl.canRender && !gl.canRender(now)) {
|
|
397
411
|
this._frameRefused = true;
|
|
398
412
|
return false;
|
|
399
413
|
}
|
package/src/idle.js
CHANGED
|
@@ -186,6 +186,17 @@ class IdleWatcher {
|
|
|
186
186
|
*/
|
|
187
187
|
async function armIdle(watcher) {
|
|
188
188
|
const session = watcher.session;
|
|
189
|
+
|
|
190
|
+
// The Windows rung, first because it is the only one there: everything
|
|
191
|
+
// below this reaches into `app.X`, and a tree that is not on an X server
|
|
192
|
+
// has none. `GetLastInputInfo` answers for the whole session, which is what
|
|
193
|
+
// an idle timeout means, so the shape is the polling rung's — with the
|
|
194
|
+
// answer arriving synchronously instead of over a connection.
|
|
195
|
+
if (typeof session.app?.lastInputMs === 'function') {
|
|
196
|
+
pollLastInput(watcher);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
const counter = await session.counter();
|
|
190
201
|
if (watcher.stopped) return;
|
|
191
202
|
|
|
@@ -281,6 +292,31 @@ function poll(watcher, saver) {
|
|
|
281
292
|
});
|
|
282
293
|
}
|
|
283
294
|
|
|
295
|
+
/**
|
|
296
|
+
* The same adaptive wait as {@link poll}, over a counter this process can read
|
|
297
|
+
* without asking anybody: `GetLastInputInfo` is a call, not a round trip.
|
|
298
|
+
*
|
|
299
|
+
* The two directions are still asymmetric for the same reason. Not idle yet:
|
|
300
|
+
* sleep for exactly the remainder. Idle: nothing says when the user will come
|
|
301
|
+
* back, and no input reaches this process while they are typing in another
|
|
302
|
+
* window, so poll on an interval scaled to the timeout.
|
|
303
|
+
*/
|
|
304
|
+
function pollLastInput(watcher) {
|
|
305
|
+
if (watcher.stopped) return;
|
|
306
|
+
const elapsed = watcher.session.app.lastInputMs?.();
|
|
307
|
+
if (typeof elapsed !== 'number') return;
|
|
308
|
+
const idle = elapsed >= watcher.timeout;
|
|
309
|
+
watcher.set(idle);
|
|
310
|
+
clearTimeout(watcher._timer);
|
|
311
|
+
watcher._timer = setTimeout(
|
|
312
|
+
() => pollLastInput(watcher),
|
|
313
|
+
idle
|
|
314
|
+
? Math.min(30_000, Math.max(1_000, watcher.timeout / 4))
|
|
315
|
+
: Math.max(250, watcher.timeout - elapsed),
|
|
316
|
+
);
|
|
317
|
+
watcher._timer.unref?.();
|
|
318
|
+
}
|
|
319
|
+
|
|
284
320
|
function schedule(watcher, saver, delay) {
|
|
285
321
|
clearTimeout(watcher._timer);
|
|
286
322
|
watcher._timer = setTimeout(() => poll(watcher, saver), delay);
|
|
@@ -364,7 +400,12 @@ export function setIdleForTests(app, timeout, idle) {
|
|
|
364
400
|
* code and for tests.
|
|
365
401
|
*/
|
|
366
402
|
export async function keepAwake({ reason = 'Busy', app = null } = {}) {
|
|
367
|
-
for (const rung of [
|
|
403
|
+
for (const rung of [
|
|
404
|
+
windowsInhibit,
|
|
405
|
+
portalInhibit,
|
|
406
|
+
screenSaverInhibit,
|
|
407
|
+
xInhibit,
|
|
408
|
+
]) {
|
|
368
409
|
try {
|
|
369
410
|
const release = await rung(reason, app);
|
|
370
411
|
if (release) return once(release);
|
|
@@ -375,6 +416,23 @@ export async function keepAwake({ reason = 'Busy', app = null } = {}) {
|
|
|
375
416
|
return () => {};
|
|
376
417
|
}
|
|
377
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Rung 0: `SetThreadExecutionState`, on Windows.
|
|
421
|
+
*
|
|
422
|
+
* Above the portal rungs because it is the only one a Windows session has,
|
|
423
|
+
* and below nothing: on a desktop with a portal this returns null on the
|
|
424
|
+
* first line and costs a property read.
|
|
425
|
+
*
|
|
426
|
+
* `reason` is dropped rather than passed. Windows takes no string with the
|
|
427
|
+
* call — `powercfg /requests` names the process, not a reason — and inventing
|
|
428
|
+
* somewhere to put it would be pretending the system shows it.
|
|
429
|
+
*/
|
|
430
|
+
async function windowsInhibit(reason, app) {
|
|
431
|
+
const hold = app?.keepAwake;
|
|
432
|
+
if (typeof hold !== 'function') return null;
|
|
433
|
+
return hold.call(app, true);
|
|
434
|
+
}
|
|
435
|
+
|
|
378
436
|
/** A release that runs once however many times it is called — a double
|
|
379
437
|
* release would drop somebody else's inhibition on the counted X rung. */
|
|
380
438
|
function once(fn) {
|
package/src/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export * from './types/fonts.js';
|
|
|
27
27
|
export * from './types/system.js';
|
|
28
28
|
export * from './types/capabilities.js';
|
|
29
29
|
export * from './types/launcher.js';
|
|
30
|
+
export * from './types/taskbar.js';
|
|
30
31
|
export * from './types/tray.js';
|
|
31
32
|
export * from './types/permissions.js';
|
|
32
33
|
export * from './types/notifications.js';
|
|
@@ -62,6 +63,46 @@ export function useWindowId(
|
|
|
62
63
|
ref: RefObject<NtkWindow | DrawnNode | null>,
|
|
63
64
|
): () => number | null;
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The handle **another process** embeds to show this window, or `null` where
|
|
68
|
+
* this backend cannot hand one out.
|
|
69
|
+
*
|
|
70
|
+
* The companion to `<window embeddable>`, and deliberately not
|
|
71
|
+
* {@link windowIdOf}: on X11 the two are the same number, and everywhere
|
|
72
|
+
* else they are not.
|
|
73
|
+
*
|
|
74
|
+
* - **X11** — the window's XID, which means the same thing in every process
|
|
75
|
+
* on the display.
|
|
76
|
+
* - **Windows** — a composition surface handle, already valid in the host
|
|
77
|
+
* process. A window cannot be embedded here (a composition target stops
|
|
78
|
+
* presenting once its window is a child), so the *buffer* crosses instead
|
|
79
|
+
* and the host binds it to a visual of its own. The host is the parent
|
|
80
|
+
* process unless `createRoot({ win32: { paneHostPid } })` says otherwise.
|
|
81
|
+
* - **Anything else** — `null`, which is the capability rather than a
|
|
82
|
+
* failure.
|
|
83
|
+
*
|
|
84
|
+
* Pass it to the host out of band, as an XID is passed: argv, an environment
|
|
85
|
+
* variable, a message. What the host does with it differs per platform; what
|
|
86
|
+
* an app writes to get it does not.
|
|
87
|
+
*/
|
|
88
|
+
export function windowHandleOf(
|
|
89
|
+
target:
|
|
90
|
+
| NtkWindow
|
|
91
|
+
| DrawnNode
|
|
92
|
+
| RefObject<NtkWindow | DrawnNode | null>
|
|
93
|
+
| null
|
|
94
|
+
| undefined,
|
|
95
|
+
): number | null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `windowHandleOf` bound to a ref. A **getter**, stable across renders, for
|
|
99
|
+
* {@link useWindowId}'s reason: the window is not realized on the render
|
|
100
|
+
* that declares it.
|
|
101
|
+
*/
|
|
102
|
+
export function useWindowHandle(
|
|
103
|
+
ref: RefObject<NtkWindow | DrawnNode | null>,
|
|
104
|
+
): () => number | null;
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* Parse a `text/uri-list` payload (RFC 2483): CRLF-separated,
|
|
67
108
|
* percent-encoded, `#` lines are comments. What `DropEvent.files` is made
|