react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// <foreign>: another process's window, inside ours.
|
|
2
|
+
//
|
|
3
|
+
// The second element that owns a real X window without painting into its
|
|
4
|
+
// parent (NEXT_STEPS §4, after `<glarea>`), and the only one that makes a
|
|
5
|
+
// react-x11 app a *host* rather than a drawer of its own pixels: a terminal
|
|
6
|
+
// pane, a video surface, a docked tray icon.
|
|
7
|
+
//
|
|
8
|
+
// The protocol is ntk's (`XEmbedSocket`, sidorares/ntk#246) — the save-set,
|
|
9
|
+
// the reparent, `_XEMBED_INFO`, the `_XEMBED` messages, the synthetic ICCCM
|
|
10
|
+
// 4.1.5 ConfigureNotify. What is left here is the part that has to be
|
|
11
|
+
// React-shaped, and it is three things:
|
|
12
|
+
//
|
|
13
|
+
// 1. **When the X calls happen.** `createInstance` runs in the render
|
|
14
|
+
// phase, which React may discard — and a ReparentWindow issued from a
|
|
15
|
+
// render that is then thrown away has moved somebody else's window for
|
|
16
|
+
// real. So this node holds no socket until the owning `WindowNode`
|
|
17
|
+
// realizes it in the commit phase, exactly as `<glarea>` is realized
|
|
18
|
+
// (`_realizeChildWindows`, nodes.js).
|
|
19
|
+
//
|
|
20
|
+
// 2. **Where the rect comes from.** Yoga, like any other child. Every
|
|
21
|
+
// change is a ConfigureWindow on the client plus the synthetic
|
|
22
|
+
// ConfigureNotify with root-relative coordinates, which is the geometry
|
|
23
|
+
// question a client actually asks.
|
|
24
|
+
//
|
|
25
|
+
// 3. **Whose focus it is.** react-x11 has a focus manager, a tab order and
|
|
26
|
+
// `preventDefault`; XEmbed has activation, logical focus and
|
|
27
|
+
// FOCUS_NEXT/PREV. This file is the mapping — see "Focus" below.
|
|
28
|
+
//
|
|
29
|
+
// Unmount hands the client back: reparented to the root, dropped from the
|
|
30
|
+
// save set, **never destroyed**. Destroying somebody else's window because a
|
|
31
|
+
// React tree changed is the failure mode this element is most able to cause,
|
|
32
|
+
// and `test/foreign.test.js` asserts against it first.
|
|
33
|
+
import { XEMBED, XEmbedSocket } from 'ntk';
|
|
34
|
+
|
|
35
|
+
import { isFocusable } from './a11y.js';
|
|
36
|
+
import { lastInputTime } from './inputtime.js';
|
|
37
|
+
import { Node, pixelFor } from './nodes.js';
|
|
38
|
+
|
|
39
|
+
const px = (v) => Math.max(1, Math.round(v || 0));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `<foreign>` — another client's top-level window, laid out as an element.
|
|
43
|
+
*
|
|
44
|
+
* ```jsx
|
|
45
|
+
* <foreign
|
|
46
|
+
* windowId={id}
|
|
47
|
+
* style={{ flexGrow: 1, backgroundColor: '#101014' }}
|
|
48
|
+
* onEmbedded={({ xembed }) => setMode(xembed ? 'xembed' : 'reparented')}
|
|
49
|
+
* onClientGone={() => respawn()}
|
|
50
|
+
* />
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* With no `windowId` the node instead **adopts** whatever is put inside it,
|
|
54
|
+
* and hands out the id to put it in through `onReady` — which is the shape
|
|
55
|
+
* `xterm -into WID` and `mpv --wid=WID` need, since the program has to be
|
|
56
|
+
* given a window before it has one of its own.
|
|
57
|
+
*
|
|
58
|
+
* Like `<glarea>`, the child X window sits above everything drawn in the
|
|
59
|
+
* parent, so 2D content cannot overlap it — a HUD belongs in a sibling
|
|
60
|
+
* `<popup>`.
|
|
61
|
+
*
|
|
62
|
+
* ### Focus
|
|
63
|
+
*
|
|
64
|
+
* The classic embedder answers XEmbed focus with a *focus proxy*: an
|
|
65
|
+
* InputOnly window that takes the real X input focus and forwards
|
|
66
|
+
* keystrokes to the client with SendEvent. This one does not, deliberately.
|
|
67
|
+
* Two reasons, and they point the same way:
|
|
68
|
+
*
|
|
69
|
+
* - A proxy inside our own toplevel means the toplevel sees FocusOut
|
|
70
|
+
* (detail Inferior) the moment the client is focused, and react-x11 reads
|
|
71
|
+
* that as the window losing focus — the caret stops blinking, `<window
|
|
72
|
+
* onBlur>` fires, and this node is told to blur the client it just
|
|
73
|
+
* focused.
|
|
74
|
+
* - A key that reaches the proxy has already gone past every handler in the
|
|
75
|
+
* React tree, so an application chord cannot be checked first. That is
|
|
76
|
+
* the one rule this element has to keep: **while a `<foreign>` holds
|
|
77
|
+
* focus, the app's handlers see the key first and everything they do not
|
|
78
|
+
* consume is forwarded**. `preventDefault()` is how they consume it, the
|
|
79
|
+
* same word that stops a `<textinput>` from eating Tab.
|
|
80
|
+
*
|
|
81
|
+
* So the X focus stays on our own window, the logical focus is a message
|
|
82
|
+
* (`XEMBED_FOCUS_IN`), and forwarding happens in `defaultKeyDown`/
|
|
83
|
+
* `defaultKeyUp` — after dispatch, which is what "the app sees it first"
|
|
84
|
+
* means mechanically.
|
|
85
|
+
*
|
|
86
|
+
* The one thing this cannot cover: X delivers a key to the deepest
|
|
87
|
+
* descendant of the focus window that contains the pointer, so while the
|
|
88
|
+
* pointer is *over* the embedded client the client receives keys directly
|
|
89
|
+
* and no handler of ours runs. Chords work everywhere else in the window,
|
|
90
|
+
* which is the trade a proxy would make in reverse.
|
|
91
|
+
*/
|
|
92
|
+
export class ForeignNode extends Node {
|
|
93
|
+
constructor(props, app) {
|
|
94
|
+
super('foreign', props, app);
|
|
95
|
+
/** ntk's XEmbedSocket, from the commit phase onwards */
|
|
96
|
+
this.socket = null;
|
|
97
|
+
/** `{ id, xembed, version }` while a client is in, else null */
|
|
98
|
+
this.client = null;
|
|
99
|
+
/** the error that stopped an embed, if one did */
|
|
100
|
+
this.error = null;
|
|
101
|
+
/** geometry last sent to the X windows */
|
|
102
|
+
this.rect = null;
|
|
103
|
+
// an embedded client is a control the user can Tab to, like a
|
|
104
|
+
// <textinput> — `focusable={false}` opts out (a video surface, say)
|
|
105
|
+
this.focusableByDefault = true;
|
|
106
|
+
// …but it is not a control we type *into*. Dead keys and Compose are
|
|
107
|
+
// ours to run only when the text lands in our tree; here the raw key
|
|
108
|
+
// event is forwarded, and the client runs whatever input method it has
|
|
109
|
+
// — an accent composed on this side would eat the KeyPress and hand
|
|
110
|
+
// back a character with nowhere to go (docs/events.md#composition).
|
|
111
|
+
this.composes = false;
|
|
112
|
+
// whether an XEMBED_FOCUS_IN is outstanding, so the matching FOCUS_OUT
|
|
113
|
+
// is sent once and only after one
|
|
114
|
+
this._focusSent = false;
|
|
115
|
+
this._backgroundPixel = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get isForeign() {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_setRoot(root) {
|
|
123
|
+
super._setRoot(root);
|
|
124
|
+
// a <foreign> mounted into a live tree realizes here; one mounted with
|
|
125
|
+
// the window is picked up by WindowNode.realize
|
|
126
|
+
if (root?.window) this.realize();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create the container X window and start embedding. Called from the
|
|
131
|
+
* commit phase and from `_setRoot`, both of which are past the point
|
|
132
|
+
* where React can discard the work.
|
|
133
|
+
*/
|
|
134
|
+
realize() {
|
|
135
|
+
if (this.socket || this.destroyed) return;
|
|
136
|
+
const parent = this.root?.window;
|
|
137
|
+
if (!parent || typeof this.app?.createWindow !== 'function') return;
|
|
138
|
+
const rect = this._geometry();
|
|
139
|
+
this.rect = rect;
|
|
140
|
+
const socket = new XEmbedSocket(parent, {
|
|
141
|
+
...rect,
|
|
142
|
+
// react-x11 has a focus manager; a socket that answered
|
|
143
|
+
// XEMBED_REQUEST_FOCUS by itself would move focus behind its back and
|
|
144
|
+
// the rest of the tree would never hear about it
|
|
145
|
+
focusOnRequest: false,
|
|
146
|
+
});
|
|
147
|
+
this.socket = socket;
|
|
148
|
+
socket.window._reactX11Node = this;
|
|
149
|
+
this._applyBackground();
|
|
150
|
+
socket.on('embedded', (info) => this._onEmbedded(info));
|
|
151
|
+
socket.on('gone', () => this._onGone());
|
|
152
|
+
socket.on('requestFocus', () => this._onRequestFocus());
|
|
153
|
+
socket.on('focusNext', () => this._moveFocus(false));
|
|
154
|
+
socket.on('focusPrev', () => this._moveFocus(true));
|
|
155
|
+
this._start(this.props.windowId);
|
|
156
|
+
// after `_start`, which maps the container itself — both paths do,
|
|
157
|
+
// because a client cannot be viewable inside an unmapped one
|
|
158
|
+
this._syncMapped();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Embed `windowId`, or — with none — wait for something to put a window
|
|
163
|
+
* inside the container and take that.
|
|
164
|
+
*
|
|
165
|
+
* The token guards the async edge: a `windowId` that changes while an
|
|
166
|
+
* embed is in flight must not have the stale one report success, and a
|
|
167
|
+
* node that unmounted must not report anything at all.
|
|
168
|
+
*/
|
|
169
|
+
_start(windowId) {
|
|
170
|
+
const socket = this.socket;
|
|
171
|
+
if (!socket) return;
|
|
172
|
+
const token = (this._token = {});
|
|
173
|
+
this.error = null;
|
|
174
|
+
const started = windowId ? socket.embed(windowId) : socket.adopt();
|
|
175
|
+
started.catch((err) => {
|
|
176
|
+
if (this._token !== token || this.destroyed) return;
|
|
177
|
+
this.error = err;
|
|
178
|
+
if (this.props.onError) this.props.onError(err);
|
|
179
|
+
else console.warn(`react-x11: <foreign> could not embed: ${err.message}`);
|
|
180
|
+
});
|
|
181
|
+
// The container id is what a program is *given* — `xterm -into ID` — so
|
|
182
|
+
// it has to be reachable before there is anything in it, and it is the
|
|
183
|
+
// only way the adopt path can ever start.
|
|
184
|
+
//
|
|
185
|
+
// On a microtask, because this runs in the **commit phase**: a handler
|
|
186
|
+
// that sets state — which is what "start the program and show its
|
|
187
|
+
// status" is — would be setting it on a component React has not finished
|
|
188
|
+
// mounting, and React says so. The microtask lands after the whole
|
|
189
|
+
// synchronous commit and still before any reply from the server, which
|
|
190
|
+
// is the only ordering the caller can observe.
|
|
191
|
+
const onReady = this.props.onReady;
|
|
192
|
+
if (onReady) {
|
|
193
|
+
queueMicrotask(() => {
|
|
194
|
+
if (this._token !== token || this.destroyed) return;
|
|
195
|
+
onReady({ windowId: socket.window.id, node: this });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_onEmbedded(info) {
|
|
201
|
+
if (this.destroyed) return;
|
|
202
|
+
this.client = { id: info.id, xembed: info.xembed, version: info.version };
|
|
203
|
+
this._syncMapped();
|
|
204
|
+
// A client that arrives while this node already has focus has missed the
|
|
205
|
+
// activation and the focus-in that went out before it was there — a
|
|
206
|
+
// terminal spawned into a focused pane, which is the ordinary case.
|
|
207
|
+
const manager = this._focusManager();
|
|
208
|
+
if (manager?.focused === this && manager.keyboardFocused)
|
|
209
|
+
this.defaultFocus();
|
|
210
|
+
this.props.onEmbedded?.({ ...this.client, node: this });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_onGone() {
|
|
214
|
+
if (this.destroyed) return;
|
|
215
|
+
this.client = null;
|
|
216
|
+
this._focusSent = false;
|
|
217
|
+
this.props.onClientGone?.({ node: this });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* `XEMBED_REQUEST_FOCUS`: the client says the user clicked it. Routed
|
|
222
|
+
* through the focus manager rather than answered here, so `:focus`,
|
|
223
|
+
* `:focus-within`, `onBlur` on whatever had focus and the AT-SPI bridge
|
|
224
|
+
* all observe it the way they observe any other focus change.
|
|
225
|
+
*/
|
|
226
|
+
_onRequestFocus() {
|
|
227
|
+
if (this.destroyed) return;
|
|
228
|
+
this.props.onRequestFocus?.({ node: this });
|
|
229
|
+
if (isFocusable(this)) this.focus();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `XEMBED_FOCUS_NEXT`/`_PREV`: the client ran off the end of its own tab
|
|
234
|
+
* chain. Continuing into ours is what makes Tab feel like one application
|
|
235
|
+
* instead of two.
|
|
236
|
+
*/
|
|
237
|
+
_moveFocus(backwards) {
|
|
238
|
+
if (this.destroyed) return;
|
|
239
|
+
const manager = this._focusManager();
|
|
240
|
+
// Only from focus we gave it. A client asking to be tabbed out of when
|
|
241
|
+
// it does not hold the focus would move focus somewhere the user is not
|
|
242
|
+
// looking, and the message is stale by definition.
|
|
243
|
+
if (manager?.focused !== this) return;
|
|
244
|
+
manager._cycleFocus(backwards);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- focus ---------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* This node has the focus *and* the toplevel has the X focus — which is
|
|
251
|
+
* exactly when EventManager calls this. Both halves of the XEmbed
|
|
252
|
+
* handshake go out: the window is active, and the client has the logical
|
|
253
|
+
* focus.
|
|
254
|
+
*
|
|
255
|
+
* `detail` is the direction Tab arrived from, which is what tells the
|
|
256
|
+
* client whether to focus its first widget or its last; a click or a
|
|
257
|
+
* `focus()` call leaves the client's own focus where it was.
|
|
258
|
+
*/
|
|
259
|
+
defaultFocus(info) {
|
|
260
|
+
const socket = this.socket;
|
|
261
|
+
if (!socket || !this.client) return;
|
|
262
|
+
const time = lastInputTime(this.app);
|
|
263
|
+
socket.activate(true, { time });
|
|
264
|
+
const detail =
|
|
265
|
+
info?.reason === 'key'
|
|
266
|
+
? info.backwards
|
|
267
|
+
? XEMBED.FOCUS_LAST
|
|
268
|
+
: XEMBED.FOCUS_FIRST
|
|
269
|
+
: XEMBED.FOCUS_CURRENT;
|
|
270
|
+
// `socket.focusIn()` would create the focus proxy — see the class
|
|
271
|
+
// comment for why this element sends the message and keeps the X focus
|
|
272
|
+
socket.send(XEMBED.FOCUS_IN, detail, 0, 0, { time });
|
|
273
|
+
this._focusSent = true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Focus left, or the toplevel stopped being the active window. */
|
|
277
|
+
defaultBlur() {
|
|
278
|
+
const socket = this.socket;
|
|
279
|
+
if (!socket) return;
|
|
280
|
+
const time = lastInputTime(this.app);
|
|
281
|
+
if (this._focusSent) {
|
|
282
|
+
socket.send(XEMBED.FOCUS_OUT, 0, 0, 0, { time });
|
|
283
|
+
this._focusSent = false;
|
|
284
|
+
}
|
|
285
|
+
socket.activate(false, { time });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
defaultKeyDown(ev) {
|
|
289
|
+
this._forwardKey(ev);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
defaultKeyUp(ev) {
|
|
293
|
+
this._forwardKey(ev);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The key, re-addressed to the client.
|
|
298
|
+
*
|
|
299
|
+
* Reached only after the whole React tree has had the event and left it
|
|
300
|
+
* undefaulted, so an application chord bound above this node has already
|
|
301
|
+
* won. Consuming it here in turn is what stops the focus cycle from also
|
|
302
|
+
* running: Tab belongs to the client, which has its own chain and says so
|
|
303
|
+
* with FOCUS_NEXT when it reaches the end of it.
|
|
304
|
+
*/
|
|
305
|
+
_forwardKey(ev) {
|
|
306
|
+
const client = this.socket?.client;
|
|
307
|
+
const native = ev.nativeEvent;
|
|
308
|
+
if (!client || client._destroyed || !native?.type) return;
|
|
309
|
+
const X = this.app?.X;
|
|
310
|
+
if (!X) return;
|
|
311
|
+
try {
|
|
312
|
+
X.SendEvent(client.id, 0, 0, {
|
|
313
|
+
...native,
|
|
314
|
+
wid: client.id,
|
|
315
|
+
child: 0,
|
|
316
|
+
// window-relative coordinates do not survive the move; the client's
|
|
317
|
+
// origin is this node's rect (root coordinates are unaffected)
|
|
318
|
+
x: (native.x ?? 0) - Math.round(this.abs.x),
|
|
319
|
+
y: (native.y ?? 0) - Math.round(this.abs.y),
|
|
320
|
+
});
|
|
321
|
+
} catch {
|
|
322
|
+
// the connection is going away; there is nothing to forward to
|
|
323
|
+
}
|
|
324
|
+
ev.preventDefault();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- layout --------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
_geometry() {
|
|
330
|
+
return {
|
|
331
|
+
x: Math.round(this.abs.x),
|
|
332
|
+
y: Math.round(this.abs.y),
|
|
333
|
+
width: px(this.abs.width),
|
|
334
|
+
height: px(this.abs.height),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
absolutize(originX, originY) {
|
|
339
|
+
super.absolutize(originX, originY);
|
|
340
|
+
this._syncGeometry();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_syncGeometry() {
|
|
344
|
+
const socket = this.socket;
|
|
345
|
+
if (!socket) return;
|
|
346
|
+
const rect = this._geometry();
|
|
347
|
+
const prev = this.rect;
|
|
348
|
+
if (
|
|
349
|
+
prev &&
|
|
350
|
+
prev.x === rect.x &&
|
|
351
|
+
prev.y === rect.y &&
|
|
352
|
+
prev.width === rect.width &&
|
|
353
|
+
prev.height === rect.height
|
|
354
|
+
) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.rect = rect;
|
|
358
|
+
// moves the container, resizes the client with it and sends the
|
|
359
|
+
// synthetic ConfigureNotify with root-relative coordinates (ICCCM 4.1.5)
|
|
360
|
+
socket.resize(rect).catch(() => {});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- props ---------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
applyProps(newProps, oldProps) {
|
|
366
|
+
const before = oldProps ?? this.props;
|
|
367
|
+
super.applyProps(newProps, oldProps);
|
|
368
|
+
if (!this.socket) return;
|
|
369
|
+
if (newProps.windowId !== before.windowId) this._reembed(before.windowId);
|
|
370
|
+
this._applyBackground();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* `windowId` changed. The old client is handed back before the new one is
|
|
375
|
+
* taken: a socket holds one client, and the window that is leaving is
|
|
376
|
+
* still somebody else's to keep.
|
|
377
|
+
*/
|
|
378
|
+
_reembed(previousId) {
|
|
379
|
+
const socket = this.socket;
|
|
380
|
+
const next = this.props.windowId;
|
|
381
|
+
// Switching between "embed this id" and "adopt whatever turns up" is a
|
|
382
|
+
// different socket, not a different client: a pending adopt() has no
|
|
383
|
+
// cancel, and would otherwise take a window out from under the id that
|
|
384
|
+
// replaced it.
|
|
385
|
+
if (!previousId !== !next) {
|
|
386
|
+
this._teardown();
|
|
387
|
+
this.realize();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this._token = {};
|
|
391
|
+
this.client = null;
|
|
392
|
+
this._focusSent = false;
|
|
393
|
+
socket
|
|
394
|
+
.release()
|
|
395
|
+
.then(() => {
|
|
396
|
+
if (this.destroyed || this.socket !== socket) return;
|
|
397
|
+
this._start(next);
|
|
398
|
+
})
|
|
399
|
+
.catch(() => {});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* What shows in the rect before a client arrives and after one leaves.
|
|
404
|
+
*
|
|
405
|
+
* It is the container window's `backgroundPixel` — the server paints it,
|
|
406
|
+
* because this element draws nothing itself. Read from the ordinary
|
|
407
|
+
* `backgroundColor` style rather than a prop of its own: a second name for
|
|
408
|
+
* the one colour it does show would leave `backgroundColor` as a style
|
|
409
|
+
* property that silently does nothing beside it.
|
|
410
|
+
*/
|
|
411
|
+
_applyBackground() {
|
|
412
|
+
const wnd = this.socket?.window;
|
|
413
|
+
if (typeof wnd?.setBackgroundPixel !== 'function') return;
|
|
414
|
+
const pixel = pixelFor(this.style.backgroundColor);
|
|
415
|
+
if (pixel === null || pixel === this._backgroundPixel) return;
|
|
416
|
+
this._backgroundPixel = pixel;
|
|
417
|
+
wnd.setBackgroundPixel(pixel);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
setHidden(hidden) {
|
|
421
|
+
super.setHidden(hidden);
|
|
422
|
+
this._syncMapped();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** The container follows `hidden`; the client follows the container,
|
|
426
|
+
* because an inferior of an unmapped window is not viewable however
|
|
427
|
+
* mapped it is itself. */
|
|
428
|
+
_syncMapped() {
|
|
429
|
+
const wnd = this.socket?.window;
|
|
430
|
+
if (!wnd) return;
|
|
431
|
+
if (this.hidden) wnd.unmap?.();
|
|
432
|
+
else wnd.map?.();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// the client's window covers this rect: nothing to paint into the
|
|
436
|
+
// parent's 2d context, and pointer events over it are the client's
|
|
437
|
+
paint() {}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Children would be drawn *under* another client's window and never seen —
|
|
441
|
+
* the same stacking rule `<glarea>` has, with no scene graph to make an
|
|
442
|
+
* exception for. Said here rather than swallowed, because an overlay is a
|
|
443
|
+
* reasonable thing to want and a sibling `<popup>` is how to have one.
|
|
444
|
+
*/
|
|
445
|
+
insertBefore(child, beforeChild) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`react-x11: <foreign> takes no children — <${child.kind}> would be ` +
|
|
448
|
+
"drawn under the embedded client's X window and never seen. Put an " +
|
|
449
|
+
'overlay in a sibling <popup>.',
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// --- teardown ------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
destroySubtree() {
|
|
456
|
+
if (this.destroyed) return;
|
|
457
|
+
super.destroySubtree();
|
|
458
|
+
this._teardown();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Give the client back, and drop the container — both **synchronously**,
|
|
463
|
+
* which is the whole reason this is not `socket.destroy()`.
|
|
464
|
+
*
|
|
465
|
+
* `XEmbedSocket.release()` asks the server where the container is before
|
|
466
|
+
* it reparents, and `destroy()` awaits that before dropping the container
|
|
467
|
+
* window. Neither wait is affordable here: `WindowNode.destroySubtree`
|
|
468
|
+
* destroys the toplevel in the same synchronous turn as this runs, and
|
|
469
|
+
* DestroyWindow takes every inferior with it — so an awaited release
|
|
470
|
+
* reparents a client that has already been destroyed along with the window
|
|
471
|
+
* it was sitting in. The save set does not cover it either: X processes
|
|
472
|
+
* that when a *connection* closes, not when a window is destroyed.
|
|
473
|
+
*
|
|
474
|
+
* So the position is computed rather than asked for (the toplevel's screen
|
|
475
|
+
* origin plus this node's rect, both already known), and the three
|
|
476
|
+
* requests go out in the order that matters — hand the client back, then
|
|
477
|
+
* destroy what it was inside. Requests on one connection are ordered, so
|
|
478
|
+
* "then" is a guarantee rather than a hope.
|
|
479
|
+
*/
|
|
480
|
+
_teardown() {
|
|
481
|
+
const socket = this.socket;
|
|
482
|
+
if (!socket) return;
|
|
483
|
+
this.socket = null;
|
|
484
|
+
this._token = {};
|
|
485
|
+
this.client = null;
|
|
486
|
+
this._focusSent = false;
|
|
487
|
+
this._backgroundPixel = null;
|
|
488
|
+
socket.removeAllListeners();
|
|
489
|
+
|
|
490
|
+
const client = socket.client;
|
|
491
|
+
// …and the socket must not act on it again: `client` is the field it
|
|
492
|
+
// publishes, and null means "nothing embedded", which is now true
|
|
493
|
+
socket.client = null;
|
|
494
|
+
try {
|
|
495
|
+
if (client && !client._destroyed) {
|
|
496
|
+
const origin = this.root?.window?._screenOrigin ?? { x: 0, y: 0 };
|
|
497
|
+
const rect = this.rect ?? this._geometry();
|
|
498
|
+
// stop selecting events on a window that is no longer ours, so the
|
|
499
|
+
// ReparentNotify below does not come back to us as `gone`
|
|
500
|
+
client.eventMask = 0;
|
|
501
|
+
this.app.X.ChangeWindowAttributes(
|
|
502
|
+
client.id,
|
|
503
|
+
{ eventMask: 0 },
|
|
504
|
+
() => {},
|
|
505
|
+
);
|
|
506
|
+
client.reparentTo(
|
|
507
|
+
this.app.rootWindow(),
|
|
508
|
+
origin.x + rect.x,
|
|
509
|
+
origin.y + rect.y,
|
|
510
|
+
);
|
|
511
|
+
client.removeFromSaveSet();
|
|
512
|
+
}
|
|
513
|
+
socket.window.destroy();
|
|
514
|
+
} catch {
|
|
515
|
+
// the connection is closing: the client goes back to the root through
|
|
516
|
+
// the save set, which is exactly what it is for
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The file a `<Frame>` forks. Everything of substance is in childmain.js
|
|
2
|
+
// behind the transport seam; this is the seam's other resident — the one
|
|
3
|
+
// transport a fork actually has, node's IPC channel, wired straight off
|
|
4
|
+
// `process`. Kept import-light and JSX-free on purpose: it runs before
|
|
5
|
+
// anything decided what loaders this process has.
|
|
6
|
+
|
|
7
|
+
import { runFrameChild } from './childmain.js';
|
|
8
|
+
|
|
9
|
+
if (typeof process.send !== 'function') {
|
|
10
|
+
console.error(
|
|
11
|
+
'react-x11: frame/child.js is the entry a <Frame> forks — it needs the ' +
|
|
12
|
+
'IPC channel fork() sets up, and cannot be run directly.',
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
runFrameChild({
|
|
18
|
+
send: (msg) => {
|
|
19
|
+
try {
|
|
20
|
+
process.send(msg);
|
|
21
|
+
} catch {
|
|
22
|
+
// the parent's end is closing; its exit handling takes it from here
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
onMessage: (cb) => {
|
|
26
|
+
process.on('message', cb);
|
|
27
|
+
return () => process.off('message', cb);
|
|
28
|
+
},
|
|
29
|
+
onDisconnect: (cb) => {
|
|
30
|
+
process.on('disconnect', cb);
|
|
31
|
+
return () => process.off('disconnect', cb);
|
|
32
|
+
},
|
|
33
|
+
});
|