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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. 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
+ });