react-x11 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/Reconciler.js +4 -14
- package/src/appcontext.js +59 -30
- package/src/cocoa/app.js +11 -0
- package/src/cocoa/glarea.js +4 -2
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/promotion.js +13 -0
- package/src/embedding.js +31 -0
- package/src/events.js +72 -12
- package/src/foreignnodes.js +56 -3
- package/src/glnodes.js +171 -40
- package/src/gloverlay.js +383 -0
- package/src/host.d.ts +0 -1
- package/src/index.d.ts +14 -1
- package/src/node.d.ts +12 -1
- package/src/nodes/window/flush.js +35 -0
- package/src/nodes/window/window.js +9 -0
- package/src/types/elements.d.ts +15 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"examples:frame": "tsx examples/frame.jsx",
|
|
61
61
|
"labs:text-baseline": "tsx examples/labs/text-baseline.jsx",
|
|
62
62
|
"labs:direct-gl": "tsx examples/labs/direct-gl.jsx",
|
|
63
|
+
"labs:gl-overlay": "tsx examples/labs/gl-overlay.jsx",
|
|
63
64
|
"examples:viewer3d": "tsx examples/viewer3d.jsx",
|
|
64
65
|
"examples:transparent": "tsx examples/transparent.jsx",
|
|
65
66
|
"examples:windows": "tsx examples/windows.jsx",
|
|
@@ -105,7 +106,7 @@
|
|
|
105
106
|
"optionalDependencies": {
|
|
106
107
|
"@windowkit/appkit": "^0.10.0",
|
|
107
108
|
"dbus-native": "^0.15.1",
|
|
108
|
-
"x11-dri": "^0.
|
|
109
|
+
"x11-dri": "^0.8.0"
|
|
109
110
|
},
|
|
110
111
|
"peerDependencies": {
|
|
111
112
|
"@babel/core": "^8.0.0",
|
package/src/Reconciler.js
CHANGED
|
@@ -138,17 +138,18 @@ const HostConfig = {
|
|
|
138
138
|
return {
|
|
139
139
|
isInsideText: false,
|
|
140
140
|
isInsideSvg: false,
|
|
141
|
-
isInside3d: false,
|
|
142
141
|
};
|
|
143
142
|
},
|
|
144
143
|
|
|
145
144
|
getChildHostContext(parentHostContext, type) {
|
|
145
|
+
// A `<glarea>`'s children are drawn nodes like any box's — 2D content
|
|
146
|
+
// above the surface (src/gloverlay.js) — so it opens no context of its
|
|
147
|
+
// own. A scene graph over the surface is `@react-x11/components/three`,
|
|
148
|
+
// with a reconciler of its own.
|
|
146
149
|
return {
|
|
147
150
|
isInsideText: parentHostContext.isInsideText || type === 'text',
|
|
148
151
|
// <svg> children are declarative SVG elements, not react-x11 nodes
|
|
149
152
|
isInsideSvg: parentHostContext.isInsideSvg || type === 'svg',
|
|
150
|
-
// inside <glarea> the children are scene nodes, not drawn nodes
|
|
151
|
-
isInside3d: parentHostContext.isInside3d || type === 'glarea',
|
|
152
153
|
};
|
|
153
154
|
},
|
|
154
155
|
|
|
@@ -194,17 +195,6 @@ const HostConfig = {
|
|
|
194
195
|
'<text> spans and strings are.',
|
|
195
196
|
);
|
|
196
197
|
}
|
|
197
|
-
if (hostContext.isInside3d) {
|
|
198
|
-
// `<glarea>` is a leaf here: it owns the surface, the frame clock and
|
|
199
|
-
// the swap, and `onDraw` is the escape hatch. A *scene graph* over it
|
|
200
|
-
// — meshes, materials, lights, post-processing, on either backend —
|
|
201
|
-
// is `@react-x11/components/three`, which brings its own reconciler.
|
|
202
|
-
throw new Error(
|
|
203
|
-
`react-x11: <${type}> is not an element — <glarea> takes no ` +
|
|
204
|
-
'children. Draw through `onDraw`, or use ' +
|
|
205
|
-
'`@react-x11/components/three` for a scene graph. See docs/gl.md.',
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
198
|
let node;
|
|
209
199
|
switch (type) {
|
|
210
200
|
case 'window':
|
package/src/appcontext.js
CHANGED
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
compositingActive,
|
|
27
27
|
watchCompositing,
|
|
28
28
|
} from './compositing.js';
|
|
29
|
+
import { canEmbed } from './embedding.js';
|
|
29
30
|
import { hasDirectGL, watchDirectGL } from './glbackend.js';
|
|
31
|
+
import { canOverlay } from './gloverlay.js';
|
|
30
32
|
|
|
31
33
|
const AppContext = createContext(null);
|
|
32
34
|
|
|
@@ -57,17 +59,29 @@ export function useApp() {
|
|
|
57
59
|
return app;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
// 'nativeControls' is a property of the backend, decided before the first
|
|
67
|
-
// render and never changing after — so its subscription has nothing to
|
|
68
|
-
// deliver and its snapshot is a property test.
|
|
62
|
+
// 'nativeControls', 'embedding' and 'glOverlay' are properties of the
|
|
63
|
+
// backend, decided before the first render and never changing after — so
|
|
64
|
+
// their subscription has nothing to deliver and their snapshot is a property
|
|
65
|
+
// test.
|
|
69
66
|
const NEVER_CHANGES = () => () => {};
|
|
70
67
|
|
|
68
|
+
// What `useSupports` watches and reads, per feature. `read` answers a
|
|
69
|
+
// boolean, so the snapshot is stable for a given state — returning the
|
|
70
|
+
// visual object for 'transparency' would tear on every render.
|
|
71
|
+
const FEATURES = {
|
|
72
|
+
transparency: {
|
|
73
|
+
watch: watchCompositing,
|
|
74
|
+
read: (app) => compositingActive(app) && Boolean(argbVisual(app)),
|
|
75
|
+
},
|
|
76
|
+
shaders: { watch: watchDirectGL, read: hasDirectGL },
|
|
77
|
+
nativeControls: {
|
|
78
|
+
watch: NEVER_CHANGES,
|
|
79
|
+
read: (app) => Boolean(app.nativeBezels),
|
|
80
|
+
},
|
|
81
|
+
embedding: { watch: NEVER_CHANGES, read: canEmbed },
|
|
82
|
+
glOverlay: { watch: NEVER_CHANGES, read: canOverlay },
|
|
83
|
+
};
|
|
84
|
+
|
|
71
85
|
/**
|
|
72
86
|
* Can this **display** do something, as a value a component can branch on?
|
|
73
87
|
*
|
|
@@ -127,40 +141,55 @@ const NEVER_CHANGES = () => () => {};
|
|
|
127
141
|
* first render already reads the final answer. A policy raised after
|
|
128
142
|
* connecting has missed that probe, and re-renders its readers when it
|
|
129
143
|
* settles rather than leaving them with two different answers.
|
|
144
|
+
*
|
|
145
|
+
* `'embedding'` is true when this connection can take another process's
|
|
146
|
+
* window into its own — the X11 backend; never Cocoa, which has no such
|
|
147
|
+
* primitive, and never the headless mock. It is the question to ask before
|
|
148
|
+
* rendering a `<foreign>`, which refuses with one `onError` where the answer
|
|
149
|
+
* is no:
|
|
150
|
+
*
|
|
151
|
+
* ```jsx
|
|
152
|
+
* const embedding = useSupports('embedding');
|
|
153
|
+
* <box style={{ flexGrow: 1 }}>
|
|
154
|
+
* {embedding ? <foreign onReady={spawnInto} /> : <text>X11 only</text>}
|
|
155
|
+
* </box>
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* Like `'nativeControls'`, it is a property of the backend and never changes.
|
|
159
|
+
*
|
|
160
|
+
* `'glOverlay'` is true when the children of a `<glarea>` are drawn above its
|
|
161
|
+
* GL surface on this connection — laid out in its box, painted on panes over
|
|
162
|
+
* the surface, hit before it. Both backends draw them; what differs is
|
|
163
|
+
* translucency, composited by Core Animation on the Cocoa backend and opaque
|
|
164
|
+
* on X11 (docs/elements.md says exactly how). It is the question to ask
|
|
165
|
+
* before handing a surface its HUD rather than drawing that some other way:
|
|
166
|
+
*
|
|
167
|
+
* ```jsx
|
|
168
|
+
* const overlay = useSupports('glOverlay');
|
|
169
|
+
* <glarea onDraw={drawMap}>{overlay && <Legend />}</glarea>
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* A property of the backend too, and it never changes.
|
|
130
173
|
*/
|
|
131
174
|
export function useSupports(feature) {
|
|
132
175
|
const app = useApp();
|
|
133
|
-
|
|
176
|
+
const spec = Object.hasOwn(FEATURES, feature) ? FEATURES[feature] : null;
|
|
177
|
+
if (!spec) {
|
|
134
178
|
throw new Error(
|
|
135
179
|
`react-x11: useSupports(${JSON.stringify(feature)}) — unknown feature ` +
|
|
136
|
-
`(expected one of ${
|
|
180
|
+
`(expected one of ${Object.keys(FEATURES).join(', ')})`,
|
|
137
181
|
);
|
|
138
182
|
}
|
|
139
|
-
//
|
|
183
|
+
// Every feature goes through the same store, so the hooks below run in the
|
|
140
184
|
// same order whatever is being asked about. Where compositing comes and
|
|
141
185
|
// goes for as long as the app runs, the backend settles at most once — and
|
|
142
186
|
// watching that one moment is what keeps two components rendered either
|
|
143
187
|
// side of it from disagreeing (see watchDirectGL).
|
|
144
188
|
const subscribe = useCallback(
|
|
145
|
-
(onChange) =>
|
|
146
|
-
|
|
147
|
-
? NEVER_CHANGES()
|
|
148
|
-
: feature === 'shaders'
|
|
149
|
-
? watchDirectGL(app, onChange)
|
|
150
|
-
: watchCompositing(app, onChange),
|
|
151
|
-
[app, feature],
|
|
152
|
-
);
|
|
153
|
-
// a boolean, so the snapshot is stable for a given state — returning the
|
|
154
|
-
// visual object here would tear on every render
|
|
155
|
-
const snapshot = useCallback(
|
|
156
|
-
() =>
|
|
157
|
-
feature === 'nativeControls'
|
|
158
|
-
? Boolean(app.nativeBezels)
|
|
159
|
-
: feature === 'shaders'
|
|
160
|
-
? hasDirectGL(app)
|
|
161
|
-
: compositingActive(app) && Boolean(argbVisual(app)),
|
|
162
|
-
[app, feature],
|
|
189
|
+
(onChange) => spec.watch(app, onChange),
|
|
190
|
+
[app, spec],
|
|
163
191
|
);
|
|
192
|
+
const snapshot = useCallback(() => spec.read(app), [app, spec]);
|
|
164
193
|
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
165
194
|
}
|
|
166
195
|
|
package/src/cocoa/app.js
CHANGED
|
@@ -26,6 +26,7 @@ import { setScreensForTests } from '../screens.js';
|
|
|
26
26
|
import { setScaleForTests } from '../scale.js';
|
|
27
27
|
import { BezelStore } from './bezels.js';
|
|
28
28
|
import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
|
|
29
|
+
import { CocoaOverlayPane } from './overlay.js';
|
|
29
30
|
import { CocoaDockMenu } from './dock.js';
|
|
30
31
|
import { CocoaGlobalMenuExport } from './globalmenu.js';
|
|
31
32
|
import { CocoaStatusItem } from './statusitem.js';
|
|
@@ -418,6 +419,16 @@ export class CocoaApp {
|
|
|
418
419
|
return cocoaGLConfig(this, spec);
|
|
419
420
|
}
|
|
420
421
|
|
|
422
|
+
/**
|
|
423
|
+
* The pane a `<glarea>`'s children are drawn on (src/gloverlay.js): one
|
|
424
|
+
* transparent layer above the surface's, which Core Animation composites —
|
|
425
|
+
* so the overlay blends with the GL frame here, where an X11 pane is an
|
|
426
|
+
* opaque child window. Having this at all is how the overlay knows.
|
|
427
|
+
*/
|
|
428
|
+
createOverlayPane(attributes) {
|
|
429
|
+
return new CocoaOverlayPane(this, attributes);
|
|
430
|
+
}
|
|
431
|
+
|
|
421
432
|
/**
|
|
422
433
|
* The Frame host seam (src/frame/index.js): a pane's composited region
|
|
423
434
|
* in this window. Its presence is what routes <Frame> to the shared-
|
package/src/cocoa/glarea.js
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
// `destroy()`, `requestAnimationFrame`. On X11 that child is a real X
|
|
15
15
|
// window stacked above the parent's drawing; here it is a sublayer of the
|
|
16
16
|
// window's root layer with a high zPosition — the same "GL sits above the
|
|
17
|
-
// 2D" semantics, by the same mechanism the platform gives us.
|
|
17
|
+
// 2D" semantics, by the same mechanism the platform gives us. The layer
|
|
18
|
+
// takes no input: pointer events are the NSWindow's, and the window's hit
|
|
19
|
+
// test answers the surface for a point over it (`GlAreaNode.hitSurface`),
|
|
20
|
+
// which is where X11's event propagation ends up too.
|
|
18
21
|
//
|
|
19
22
|
// ## The API ladder
|
|
20
23
|
//
|
|
@@ -142,7 +145,6 @@ export class CocoaGLArea {
|
|
|
142
145
|
this.scale = this.parent.scale ?? app.scale ?? 1;
|
|
143
146
|
this.destroyed = false;
|
|
144
147
|
this._reactX11Node = null;
|
|
145
|
-
this.onWheel = options.onWheel ?? null;
|
|
146
148
|
this.layer = this._native.createLayer();
|
|
147
149
|
this._native.addSublayer(this.parent._layer, this.layer);
|
|
148
150
|
this.rect = null;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The pane a `<glarea>`'s children are drawn on, on the Cocoa backend: a
|
|
2
|
+
// transparent bitmap layer over the surface (src/gloverlay.js).
|
|
3
|
+
//
|
|
4
|
+
// A sublayer of the window's root layer like the surface's own
|
|
5
|
+
// (src/cocoa/glarea.js), one step above it: the GL layer sits at zPosition
|
|
6
|
+
// 1e7, over everything both presenters put on the root layer, and this one
|
|
7
|
+
// at 1e7 + 1, over that. Core Animation composites it, so what the children
|
|
8
|
+
// leave transparent shows the GL frame, and a translucent fill, an
|
|
9
|
+
// antialiased edge or a shadow blends with it — the one backend where the
|
|
10
|
+
// overlay is not opaque. Every surface's GL layer shares the one zPosition,
|
|
11
|
+
// so where two surfaces overlap, both overlays are above both frames.
|
|
12
|
+
//
|
|
13
|
+
// It speaks the verbs of a window the overlay drives on X11 — `setState`,
|
|
14
|
+
// `map`, `unmap`, `getContext`, `destroy` — plus `present`, which puts what
|
|
15
|
+
// was painted on the layer: ntk blits an X window's backing store on its
|
|
16
|
+
// own, where a layer's contents are a copy the bitmap has to be pushed to.
|
|
17
|
+
import { CocoaContext2D } from './context2d.js';
|
|
18
|
+
|
|
19
|
+
export const OVERLAY_Z = 1e7 + 1;
|
|
20
|
+
|
|
21
|
+
export class CocoaOverlayPane {
|
|
22
|
+
constructor(app, options) {
|
|
23
|
+
this.app = app;
|
|
24
|
+
this.parent = options.parent;
|
|
25
|
+
this._native = app._native;
|
|
26
|
+
this.scale = this.parent.scale ?? app.scale ?? 1;
|
|
27
|
+
this.destroyed = false;
|
|
28
|
+
this.layer = this._native.createLayer();
|
|
29
|
+
this._native.addSublayer(this.parent._layer, this.layer);
|
|
30
|
+
this.rect = null;
|
|
31
|
+
this._surface = null;
|
|
32
|
+
this._surfaceSize = null;
|
|
33
|
+
this._gen = 0;
|
|
34
|
+
this._ctx = null;
|
|
35
|
+
this._dirty = false;
|
|
36
|
+
// Hidden until something is on it: a layer shows its contents from the
|
|
37
|
+
// moment it is added, and before the first present there are none to
|
|
38
|
+
// show, only whatever the render server makes of that.
|
|
39
|
+
this._presented = false;
|
|
40
|
+
this._hidden = false;
|
|
41
|
+
this.setState({
|
|
42
|
+
x: options.x ?? 0,
|
|
43
|
+
y: options.y ?? 0,
|
|
44
|
+
width: options.width ?? 1,
|
|
45
|
+
height: options.height ?? 1,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get width() {
|
|
50
|
+
return this.rect?.width ?? 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get height() {
|
|
54
|
+
return this.rect?.height ?? 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Geometry in device px, the unit the overlay's rects are in. */
|
|
58
|
+
setState(rect) {
|
|
59
|
+
if (this.destroyed) return;
|
|
60
|
+
this.rect = rect;
|
|
61
|
+
const s = this.scale;
|
|
62
|
+
this._setLayerProps({
|
|
63
|
+
frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
|
|
64
|
+
zPosition: OVERLAY_Z,
|
|
65
|
+
hidden: this._hidden || !this._presented,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
map() {
|
|
70
|
+
this._hidden = false;
|
|
71
|
+
if (this._presented) this._setLayerProps({ hidden: false });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
unmap() {
|
|
75
|
+
this._hidden = true;
|
|
76
|
+
this._setLayerProps({ hidden: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Implicit animations off, for the reason `CocoaGLArea._setLayerProps`
|
|
80
|
+
* gives: no frame's transaction covers a layer that is not a presenter's. */
|
|
81
|
+
_setLayerProps(props) {
|
|
82
|
+
if (this.destroyed) return;
|
|
83
|
+
const native = this._native;
|
|
84
|
+
native.txBegin({ disableActions: true });
|
|
85
|
+
try {
|
|
86
|
+
native.setLayerProps(this.layer, props);
|
|
87
|
+
} finally {
|
|
88
|
+
native.txCommit();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The bitmap, the pane's size — a new size is a new bitmap, cleared, and
|
|
93
|
+
* the old one freed now rather than by the handle's finalizer. */
|
|
94
|
+
_ensureSurface() {
|
|
95
|
+
const w = Math.max(1, this.rect?.width ?? 1);
|
|
96
|
+
const h = Math.max(1, this.rect?.height ?? 1);
|
|
97
|
+
const size = this._surfaceSize;
|
|
98
|
+
if (!this._surface || size.width !== w || size.height !== h) {
|
|
99
|
+
this._release();
|
|
100
|
+
this._surface = this._native.createSurface(w, h, this.scale);
|
|
101
|
+
this._native.ctxClearRect(this._surface, 0, 0, w, h);
|
|
102
|
+
this._surfaceSize = { width: w, height: h };
|
|
103
|
+
this._gen++;
|
|
104
|
+
}
|
|
105
|
+
return this._surface;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_release() {
|
|
109
|
+
const surface = this._surface;
|
|
110
|
+
this._surface = null;
|
|
111
|
+
if (surface && typeof this._native.releaseSurface === 'function') {
|
|
112
|
+
this._native.releaseSurface(surface);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getContext() {
|
|
117
|
+
if (!this._ctx) {
|
|
118
|
+
this._ctx = new CocoaContext2D(
|
|
119
|
+
this._native,
|
|
120
|
+
() => this._ensureSurface(),
|
|
121
|
+
() => {
|
|
122
|
+
this._ensureSurface();
|
|
123
|
+
return this._gen;
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
this._ctx._fonts = this.app.fonts;
|
|
127
|
+
this._ctx._onDirty = () => {
|
|
128
|
+
this._dirty = true;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return this._ctx;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** What was painted, onto the layer — a copy, so the bitmap is free to be
|
|
135
|
+
* painted again at once. */
|
|
136
|
+
present() {
|
|
137
|
+
if (this.destroyed || !this._dirty || !this._surface) return;
|
|
138
|
+
this._dirty = false;
|
|
139
|
+
const native = this._native;
|
|
140
|
+
native.txBegin({ disableActions: true });
|
|
141
|
+
try {
|
|
142
|
+
native.surfaceToLayer(this._surface, this.layer);
|
|
143
|
+
if (!this._presented) {
|
|
144
|
+
this._presented = true;
|
|
145
|
+
if (!this._hidden) native.setLayerProps(this.layer, { hidden: false });
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
native.txCommit();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
destroy() {
|
|
153
|
+
if (this.destroyed) return;
|
|
154
|
+
this.destroyed = true;
|
|
155
|
+
this._release();
|
|
156
|
+
this._ctx = null;
|
|
157
|
+
this._native.removeFromSuperlayer(this.layer);
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -142,6 +142,18 @@ function paintsSomething(node) {
|
|
|
142
142
|
return Boolean(node.isScroller?.());
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Inside a `<glarea>`: drawn on a pane above the surface (src/gloverlay.js).
|
|
147
|
+
* A layer of its own would sit on the root layer *under* the GL layer, so a
|
|
148
|
+
* promoted node there would vanish behind the surface it is drawn over.
|
|
149
|
+
*/
|
|
150
|
+
function insideGlArea(node) {
|
|
151
|
+
for (let n = node.parent; n && !n.isWindow; n = n.parent) {
|
|
152
|
+
if (n.isGlArea) return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
/**
|
|
146
158
|
* Can this node be a property box on a layer at all — the static half of
|
|
147
159
|
* the answer, the same whatever the scene around it does: a plain box by
|
|
@@ -150,6 +162,7 @@ function paintsSomething(node) {
|
|
|
150
162
|
*/
|
|
151
163
|
function promotableNode(node) {
|
|
152
164
|
if (node.destroyed || !plainBox(node)) return false;
|
|
165
|
+
if (insideGlArea(node)) return false;
|
|
153
166
|
if (!stylePaintsPlain(node, node._targetStyle ?? node.style)) return false;
|
|
154
167
|
if (node.isScroller?.()) return false;
|
|
155
168
|
return !paintsOutline(node);
|
package/src/embedding.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Whether a connection can take another process's window into its own.
|
|
2
|
+
//
|
|
3
|
+
// `<foreign>` asks before it builds a socket, and `useSupports('embedding')`
|
|
4
|
+
// asks for a component deciding whether to render one. One function for
|
|
5
|
+
// both, because the two have to agree: a hook that said yes over an element
|
|
6
|
+
// that then refused, or the reverse, is the bug this closes (issue #531).
|
|
7
|
+
//
|
|
8
|
+
// A feature test on the connection, not a question about which backend this
|
|
9
|
+
// is — and not "is there an `X`", which is true everywhere: the Cocoa app
|
|
10
|
+
// carries an X stub for the modules with an X escape hatch, and the headless
|
|
11
|
+
// mock carries the same one. Both also have a `createWindow` that takes a
|
|
12
|
+
// parent, which is how `<foreign>` used to get as far as a socket over them.
|
|
13
|
+
// So each part of embedding is asked about by name: a container window to
|
|
14
|
+
// hold the client, `ReparentWindow` to move somebody else's window into it,
|
|
15
|
+
// and the save set, which keeps that window alive if this process dies
|
|
16
|
+
// holding it — the promise docs/embedding.md makes about a window we do not
|
|
17
|
+
// own.
|
|
18
|
+
//
|
|
19
|
+
// A leaf module on purpose, for glbackend.js's reason: it imports nothing of
|
|
20
|
+
// ours, so both `appcontext.js` and the node layer can use it without the
|
|
21
|
+
// two importing each other.
|
|
22
|
+
|
|
23
|
+
/** Can `app` host another client's window inside one of its own? */
|
|
24
|
+
export function canEmbed(app) {
|
|
25
|
+
const X = app?.X;
|
|
26
|
+
return (
|
|
27
|
+
typeof app?.createWindow === 'function' &&
|
|
28
|
+
typeof X?.ReparentWindow === 'function' &&
|
|
29
|
+
typeof X?.ChangeSaveSet === 'function'
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/events.js
CHANGED
|
@@ -47,6 +47,10 @@ const WHEEL_BUTTONS = new Set([4, 5, 6, 7]);
|
|
|
47
47
|
*/
|
|
48
48
|
export const WHEEL_NOTCH_PX = 48;
|
|
49
49
|
const RIGHT_BUTTON = 3;
|
|
50
|
+
// A LeaveNotify's detail (core protocol): the pointer went to an ancestor of
|
|
51
|
+
// the window it left, or into a window inside it — see `_leftIntoSurface`.
|
|
52
|
+
const NOTIFY_ANCESTOR = 0;
|
|
53
|
+
const NOTIFY_INFERIOR = 2;
|
|
50
54
|
|
|
51
55
|
/** How many leading entries two node paths share. */
|
|
52
56
|
function sharedPrefix(a, b) {
|
|
@@ -637,8 +641,32 @@ export class EventManager {
|
|
|
637
641
|
return node.isWindow ? node.window : node;
|
|
638
642
|
}
|
|
639
643
|
|
|
644
|
+
/**
|
|
645
|
+
* The node a pointer event landed on: a surface first, then the tree,
|
|
646
|
+
* front to back. A `<glarea>` is stacked above every 2D thing in its
|
|
647
|
+
* window, so a point inside one is over it whatever the tree's order says
|
|
648
|
+
* (`GlAreaNode.hitSurface`).
|
|
649
|
+
*/
|
|
640
650
|
_hit(ev) {
|
|
641
|
-
return
|
|
651
|
+
return (
|
|
652
|
+
this._surfaceAt(ev.x, ev.y) ?? this.node.hitTest(ev.x, ev.y) ?? this.node
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The frontmost of this window's surfaces a point is over, or null. The
|
|
658
|
+
* newest is on top on both backends (`GlAreaNode._create`). An empty list
|
|
659
|
+
* is one length read on the motion path, which is all a window with no
|
|
660
|
+
* `<glarea>` pays for this.
|
|
661
|
+
*/
|
|
662
|
+
_surfaceAt(x, y) {
|
|
663
|
+
const surfaces = this.node._surfaces;
|
|
664
|
+
if (!surfaces?.length) return null;
|
|
665
|
+
for (let i = surfaces.length - 1; i >= 0; i--) {
|
|
666
|
+
const hit = surfaces[i].hitSurface(x, y);
|
|
667
|
+
if (hit) return hit;
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
642
670
|
}
|
|
643
671
|
|
|
644
672
|
_path(target) {
|
|
@@ -742,16 +770,7 @@ export class EventManager {
|
|
|
742
770
|
* way somewhere rather than one, and React must be free to interrupt the
|
|
743
771
|
* render it started for the notch before.
|
|
744
772
|
*/
|
|
745
|
-
|
|
746
|
-
* @param {object} native ntk's wheel event, in this window's coordinates
|
|
747
|
-
* @param {object} [over] the node the wheel happened over, when the caller
|
|
748
|
-
* already knows. A `<glarea>` owns its own X window, so the server
|
|
749
|
-
* delivers the event *there* and the surface hands it back translated
|
|
750
|
-
* (src/glnodes.js) — and a hit test would answer with the box behind it,
|
|
751
|
-
* because a window-owning child is not in its parent's paint order.
|
|
752
|
-
* Knowing beats guessing; everything after this line is the same.
|
|
753
|
-
*/
|
|
754
|
-
_onWheel(native, over = null) {
|
|
773
|
+
_onWheel(native) {
|
|
755
774
|
// The first wheel is what says this window wants smooth scrolling. It was
|
|
756
775
|
// created on core events — an XI2 selection costs four times as many
|
|
757
776
|
// bytes per *pointer move*, which a window that is never scrolled would
|
|
@@ -765,7 +784,7 @@ export class EventManager {
|
|
|
765
784
|
// outside gets, and for the same reason (`_pressOutside`).
|
|
766
785
|
if (this._dismissOutside(native)) return;
|
|
767
786
|
runWithPriority(ContinuousEventPriority, () => {
|
|
768
|
-
const target =
|
|
787
|
+
const target = this._hit(native);
|
|
769
788
|
// Shift turns a vertical wheel sideways — the convention for the mouse
|
|
770
789
|
// and the touchpad that have no horizontal axis. Read off the delta
|
|
771
790
|
// rather than off the source: a plain wheel mouse on an XI2 connection
|
|
@@ -1043,6 +1062,13 @@ export class EventManager {
|
|
|
1043
1062
|
}
|
|
1044
1063
|
|
|
1045
1064
|
_onMouseOut(native) {
|
|
1065
|
+
// The pointer went into one of this window's surfaces, not out of the
|
|
1066
|
+
// window: X reports crossing into a child window as leaving this one,
|
|
1067
|
+
// but the input over a surface still arrives here — it selects none of
|
|
1068
|
+
// its own — and the next motion names it. Answered as a leave, every
|
|
1069
|
+
// ancestor was told the pointer had gone and then that it was back, and
|
|
1070
|
+
// `onMouseOut` that it had left a window it was still in.
|
|
1071
|
+
if (this._leftIntoSurface(native)) return;
|
|
1046
1072
|
runWithPriority(ContinuousEventPriority, () => {
|
|
1047
1073
|
this._updateHover([], native);
|
|
1048
1074
|
// the pointer is somewhere else entirely: whatever it was heading for
|
|
@@ -1061,6 +1087,40 @@ export class EventManager {
|
|
|
1061
1087
|
});
|
|
1062
1088
|
}
|
|
1063
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* Whether a LeaveNotify leaves the pointer in one of this window's
|
|
1092
|
+
* surfaces rather than out of the window.
|
|
1093
|
+
*
|
|
1094
|
+
* A real server says "into a window inside this one" with detail
|
|
1095
|
+
* NotifyInferior, and says it twice over a surface: when the pointer moves
|
|
1096
|
+
* onto it, and when a grab ends with the pointer on it (mode NotifyUngrab)
|
|
1097
|
+
* — which every click and every wheel notch over a surface does, since the
|
|
1098
|
+
* press's implicit grab is this window's. node-x11's in-process server
|
|
1099
|
+
* says it with detail NotifyAncestor and the child named instead, and
|
|
1100
|
+
* sends no crossings for grabs at all.
|
|
1101
|
+
*
|
|
1102
|
+
* Either way the point says which inferior, and only a surface's rect
|
|
1103
|
+
* makes it ours: a nested `<window>` or a `<foreign>` takes its own input,
|
|
1104
|
+
* so leaving for one of those is still a leave. So is a grab taken
|
|
1105
|
+
* elsewhere while the pointer is over a surface — detail NotifyVirtual or
|
|
1106
|
+
* NonlinearVirtual, the surface named as the child it left from — since
|
|
1107
|
+
* the pointer is the grab's now. The Cocoa backend's leave carries no
|
|
1108
|
+
* detail, and is always one.
|
|
1109
|
+
*/
|
|
1110
|
+
_leftIntoSurface(native) {
|
|
1111
|
+
if (!native) return false;
|
|
1112
|
+
const into =
|
|
1113
|
+
native.detail === NOTIFY_INFERIOR ||
|
|
1114
|
+
(native.detail === NOTIFY_ANCESTOR && Boolean(native.child));
|
|
1115
|
+
if (!into) return false;
|
|
1116
|
+
const wnd = this.node.window;
|
|
1117
|
+
const { x, y } = native;
|
|
1118
|
+
if (x < 0 || y < 0 || x >= (wnd?.width ?? 0) || y >= (wnd?.height ?? 0)) {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
return this._surfaceAt(x, y) !== null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1064
1124
|
/**
|
|
1065
1125
|
* The chain of the press that is still live, as far along it as the
|
|
1066
1126
|
* pointer has stayed. `downPath` is where the press landed and does not
|
package/src/foreignnodes.js
CHANGED
|
@@ -33,12 +33,18 @@
|
|
|
33
33
|
import { XEMBED, XEmbedSocket } from 'ntk';
|
|
34
34
|
|
|
35
35
|
import { isFocusable } from './a11y.js';
|
|
36
|
+
import { canEmbed } from './embedding.js';
|
|
36
37
|
import { lastInputTime } from './inputtime.js';
|
|
37
38
|
import { Node } from './nodes/node.js';
|
|
38
39
|
import { pixelFor } from './nodes/window/capabilities.js';
|
|
39
40
|
|
|
40
41
|
const px = (v) => Math.max(1, Math.round(v || 0));
|
|
41
42
|
|
|
43
|
+
// Apps already told, by a <foreign> with no `onError`, that they cannot
|
|
44
|
+
// embed. Once per app: the answer belongs to the backend, so the second pane
|
|
45
|
+
// to ask carries no news — the same rule as a display with no ARGB visual.
|
|
46
|
+
const warnedCannotEmbed = new WeakSet();
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* `<foreign>` — another client's top-level window, laid out as an element.
|
|
44
50
|
*
|
|
@@ -89,6 +95,18 @@ const px = (v) => Math.max(1, Math.round(v || 0));
|
|
|
89
95
|
* pointer is *over* the embedded client the client receives keys directly
|
|
90
96
|
* and no handler of ours runs. Chords work everywhere else in the window,
|
|
91
97
|
* which is the trade a proxy would make in reverse.
|
|
98
|
+
*
|
|
99
|
+
* ### Where there is no embedding
|
|
100
|
+
*
|
|
101
|
+
* All of this needs a connection that can reparent somebody else's window,
|
|
102
|
+
* and two apps this node meets have none: the Cocoa backend and the headless
|
|
103
|
+
* mock. Both carry an X stub and a `createWindow` that takes a parent, which
|
|
104
|
+
* used to be enough to build a socket over them — on Cocoa around a child GL
|
|
105
|
+
* surface whose `id` is undefined, handed out through `onReady` and spawned
|
|
106
|
+
* `xterm -into undefined` with; on the mock, a throw from inside the commit.
|
|
107
|
+
* So `realize()` asks `canEmbed` first, the question
|
|
108
|
+
* `useSupports('embedding')` answers too, and a no is final: no container, no
|
|
109
|
+
* `onReady`, one `onError`, and an empty box in the layout.
|
|
92
110
|
*/
|
|
93
111
|
export class ForeignNode extends Node {
|
|
94
112
|
constructor(props, app) {
|
|
@@ -97,8 +115,12 @@ export class ForeignNode extends Node {
|
|
|
97
115
|
this.socket = null;
|
|
98
116
|
/** `{ id, xembed, version }` while a client is in, else null */
|
|
99
117
|
this.client = null;
|
|
100
|
-
/** the error that stopped an embed, if one did
|
|
118
|
+
/** the error that stopped an embed, if one did — or, where this
|
|
119
|
+
* connection cannot embed at all, the refusal */
|
|
101
120
|
this.error = null;
|
|
121
|
+
// set once `realize()` finds this connection cannot embed, and final:
|
|
122
|
+
// the backend does not change under a node
|
|
123
|
+
this._refused = false;
|
|
102
124
|
/** geometry last sent to the X windows */
|
|
103
125
|
this.rect = null;
|
|
104
126
|
// an embedded client is a control the user can Tab to, like a
|
|
@@ -133,9 +155,10 @@ export class ForeignNode extends Node {
|
|
|
133
155
|
* where React can discard the work.
|
|
134
156
|
*/
|
|
135
157
|
realize() {
|
|
136
|
-
if (this.socket || this.destroyed) return;
|
|
158
|
+
if (this.socket || this.destroyed || this._refused) return;
|
|
137
159
|
const parent = this.root?.window;
|
|
138
|
-
if (!parent
|
|
160
|
+
if (!parent) return;
|
|
161
|
+
if (!canEmbed(this.app)) return this._refuse();
|
|
139
162
|
const rect = this._geometry();
|
|
140
163
|
this.rect = rect;
|
|
141
164
|
const socket = new XEmbedSocket(parent, {
|
|
@@ -159,6 +182,36 @@ export class ForeignNode extends Node {
|
|
|
159
182
|
this._syncMapped();
|
|
160
183
|
}
|
|
161
184
|
|
|
185
|
+
/**
|
|
186
|
+
* This connection cannot embed: say so once, and be an empty box.
|
|
187
|
+
*
|
|
188
|
+
* On a microtask for `onReady`'s reason: this runs in the commit phase, and
|
|
189
|
+
* the ordinary answer to the news is to set state. Nothing else needs
|
|
190
|
+
* undoing — with no socket, every other path through this node (layout,
|
|
191
|
+
* props, focus, keys, teardown) already finds nothing to act on.
|
|
192
|
+
*/
|
|
193
|
+
_refuse() {
|
|
194
|
+
this._refused = true;
|
|
195
|
+
const err = new Error(
|
|
196
|
+
'react-x11: <foreign> needs the X11 backend — this one has no ' +
|
|
197
|
+
'cross-process window embedding, so nothing can be put in it. Ask ' +
|
|
198
|
+
"useSupports('embedding') before rendering one.",
|
|
199
|
+
);
|
|
200
|
+
this.error = err;
|
|
201
|
+
// The client is the whole reason this node is a Tab stop by default, and
|
|
202
|
+
// one that can never arrive would leave a dead one. `focusable` still
|
|
203
|
+
// wins, as it does on a box.
|
|
204
|
+
this.focusableByDefault = false;
|
|
205
|
+
queueMicrotask(() => {
|
|
206
|
+
if (this.destroyed) return;
|
|
207
|
+
if (this.props.onError) this.props.onError(err);
|
|
208
|
+
else if (this.app && !warnedCannotEmbed.has(this.app)) {
|
|
209
|
+
warnedCannotEmbed.add(this.app);
|
|
210
|
+
console.warn(err.message);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
162
215
|
/**
|
|
163
216
|
* Embed `windowId`, or — with none — wait for something to put a window
|
|
164
217
|
* inside the container and take that.
|