react-x11 2.51.0 → 2.52.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 +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Embedding other applications: `<foreign>`
|
|
2
|
+
|
|
3
|
+
> **X11 only.** Cross-process window embedding does not exist on macOS:
|
|
4
|
+
> there is no way to take another application's window into your own view
|
|
5
|
+
> hierarchy, and this is not a gap waiting to be filled — the platform does
|
|
6
|
+
> not offer the primitive. On the Cocoa backend `<foreign>` refuses: it lays
|
|
7
|
+
> out as an empty box, never calls `onReady`, and calls `onError` once — and
|
|
8
|
+
> `useSupports('embedding')` is false there, so a component can ask before
|
|
9
|
+
> rendering one. What does work there is [`<Frame>`](frame.md), which embeds
|
|
10
|
+
> a pane of _your own_ application, and reaches the same place through
|
|
11
|
+
> shared surfaces instead of reparenting.
|
|
12
|
+
|
|
13
|
+
Everything else react-x11 draws, it draws itself. `<foreign>` is the one
|
|
14
|
+
element that shows somebody else's pixels: another process's top-level X
|
|
15
|
+
window, reparented into yours and laid out like any other child.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<foreign
|
|
19
|
+
windowId={terminal.windowId}
|
|
20
|
+
style={{ flexGrow: 1, backgroundColor: '#101014' }}
|
|
21
|
+
onEmbedded={({ xembed }) => setMode(xembed ? 'xembed' : 'reparented')}
|
|
22
|
+
onClientGone={() => respawn()}
|
|
23
|
+
/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That is a terminal pane, a video surface, a docked tray icon and a
|
|
27
|
+
control-panel applet — four things an application cannot draw for itself and
|
|
28
|
+
has no business reimplementing.
|
|
29
|
+
|
|
30
|
+
When the other process is a module of **your own application**, don't wire
|
|
31
|
+
this up by hand: [`<Frame>`](frame.md) is `<foreign>` plus the process — the
|
|
32
|
+
fork, the props, the callbacks, the bridged theme, the crash fallback. This
|
|
33
|
+
page is the window half, and everything below about stacking, focus and
|
|
34
|
+
teardown holds for a `<Frame>` pane too. The pane's own root window opts in
|
|
35
|
+
with `<window embeddable>`: created **unmapped** — a window waiting to be
|
|
36
|
+
embedded is unmapped, that is what waiting looks like — and mapped by the
|
|
37
|
+
embedder once reparented. On X11 it is an XEmbed client for Tab: `FOCUS_IN`
|
|
38
|
+
with `FIRST` or `LAST` lands on its first or last stop, and Tab off an end of
|
|
39
|
+
its order sends `FOCUS_NEXT` or `FOCUS_PREV`, so Tab goes through a
|
|
40
|
+
react-x11 guest as it goes through any subtree of the host's.
|
|
41
|
+
|
|
42
|
+
**Being the guest yourself** — a react-x11 app embedded in somebody else's UI
|
|
43
|
+
— is that same prop plus one accessor: `windowHandleOf(ref)` (or
|
|
44
|
+
`useWindowHandle(ref)`) is the number you hand the host, out of band, the way
|
|
45
|
+
`xterm -into` is handed one. On X11 it is the window's XID and the host
|
|
46
|
+
reparents it. On Windows it is a composition surface handle and the host
|
|
47
|
+
binds it to a visual of its own, because a window there stops presenting the
|
|
48
|
+
moment it becomes a child ([windows-embedding.md](windows-embedding.md) has
|
|
49
|
+
the measurements). `null` means this backend has no way to be a guest — the
|
|
50
|
+
capability, not a failure.
|
|
51
|
+
|
|
52
|
+
The protocol underneath is [XEmbed](http://specifications.freedesktop.org/xembed/0.5/),
|
|
53
|
+
implemented in ntk (`XEmbedSocket`, ntk ≥ 7.4.0). This page is the React half:
|
|
54
|
+
what the element does, what it promises about a window it does not own, and
|
|
55
|
+
what it does with the keyboard.
|
|
56
|
+
|
|
57
|
+
## Two ways in
|
|
58
|
+
|
|
59
|
+
**You have a window id.** Pass it as `windowId`. The client is added to the
|
|
60
|
+
save set, reparented into the node's own X window, sized to the node's rect
|
|
61
|
+
and told where it is.
|
|
62
|
+
|
|
63
|
+
**You are about to start the program.** Then there is no id yet — a program
|
|
64
|
+
like `xterm -into WID` or `mpv --wid=WID` has to be _given_ a window before it
|
|
65
|
+
makes one. Leave `windowId` out, and the node adopts whatever turns up inside
|
|
66
|
+
it; `onReady` hands you the id to spawn into:
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
<foreign
|
|
70
|
+
style={{ flexGrow: 1 }}
|
|
71
|
+
onReady={({ windowId }) => spawn('xterm', ['-into', String(windowId)])}
|
|
72
|
+
onClientGone={() => close(paneId)}
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`onReady` fires as soon as the node has an X window, which is before anything
|
|
77
|
+
is in it — that is the point.
|
|
78
|
+
|
|
79
|
+
## Most clients do not speak XEmbed
|
|
80
|
+
|
|
81
|
+
`xterm -into`, `mpv --wid`, VLC and Wine set no `_XEMBED_INFO` and want plain
|
|
82
|
+
reparenting. A missing property is not an error and not a fallback: it is the
|
|
83
|
+
ordinary case, and it is what makes a terminal pane work at all. `onEmbedded`
|
|
84
|
+
reports which happened:
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
onEmbedded={({ id, xembed, version }) => { … }}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`xembed: false` means plain reparenting — the client was mapped immediately
|
|
91
|
+
and no `_XEMBED` messages will pass. `xembed: true` means the client answered,
|
|
92
|
+
`version` is the protocol version both sides settled on, and from then on the
|
|
93
|
+
client is mapped and unmapped as its own `XEMBED_MAPPED` bit asks.
|
|
94
|
+
|
|
95
|
+
## The window is not yours
|
|
96
|
+
|
|
97
|
+
Unmounting a `<foreign>` **reparents the client back to the root window and
|
|
98
|
+
drops it from the save set. It never destroys it.** A React tree changing
|
|
99
|
+
shape is not a reason for another application to lose its window, and the
|
|
100
|
+
first test in `test/foreign.test.js` is there to keep it that way.
|
|
101
|
+
|
|
102
|
+
The same holds for `windowId` changing: the client that is leaving is handed
|
|
103
|
+
back before the new one is taken.
|
|
104
|
+
|
|
105
|
+
What this costs is an ordering rule, and it is the one thing worth knowing
|
|
106
|
+
about the implementation: the reparent is issued **synchronously** during
|
|
107
|
+
teardown, because `WindowNode` destroys its own X window in the same turn and
|
|
108
|
+
`DestroyWindow` takes every inferior with it. The save set does not cover
|
|
109
|
+
that — X processes it when a _connection_ closes, not when a window is
|
|
110
|
+
destroyed.
|
|
111
|
+
|
|
112
|
+
`onClientGone` fires when the client is destroyed by its own process, or
|
|
113
|
+
reparented away by someone else. Nothing further is sent to a dead id.
|
|
114
|
+
|
|
115
|
+
### Letting go really means letting go
|
|
116
|
+
|
|
117
|
+
A released client is a top-level window again, and **a running window manager
|
|
118
|
+
will manage it** — frame it, put it in the taskbar, place it wherever its
|
|
119
|
+
policy says. That is the right outcome for an unmount (`examples/foreign.jsx`
|
|
120
|
+
is built on it: detach the pane and the terminal is simply a terminal window
|
|
121
|
+
on the desktop), and it is a trap for one thing:
|
|
122
|
+
|
|
123
|
+
> **Do not hand a client from one `<foreign>` to another by unmounting the
|
|
124
|
+
> first and mounting the second with the same `windowId`.**
|
|
125
|
+
|
|
126
|
+
The window is parked at the root for the moment in between, which is long
|
|
127
|
+
enough to be offered to the window manager. The WM's `ReparentWindow` into a
|
|
128
|
+
frame of its own then arrives _after_ the second pane has taken the client,
|
|
129
|
+
undoes it, and the second pane reports `onClientGone` — for a client that is
|
|
130
|
+
alive and now framed on the desktop. It is a race with another process, so it
|
|
131
|
+
does not always happen, which is worse than if it always did.
|
|
132
|
+
|
|
133
|
+
Change `windowId` on **one** node instead. That path releases and re-embeds
|
|
134
|
+
in order and is tested; the client is only ever at the root while nothing
|
|
135
|
+
else wants it. (Re-embedding a window the WM has already framed also works —
|
|
136
|
+
the reparent pulls it out and the WM unmanages it — the hazard is only the
|
|
137
|
+
moment right after a release.)
|
|
138
|
+
|
|
139
|
+
## Layout and stacking
|
|
140
|
+
|
|
141
|
+
The rect comes from yoga like any other child, and every change moves the
|
|
142
|
+
client and sends it the synthetic ICCCM 4.1.5 `ConfigureNotify` — root-relative
|
|
143
|
+
coordinates, which is the question a client actually asks and which its own
|
|
144
|
+
`ConfigureNotify` (relative to the container it now sits in) cannot answer.
|
|
145
|
+
|
|
146
|
+
Two consequences of it being a real X window, both shared with
|
|
147
|
+
[`<glarea>`](elements.md#glarea):
|
|
148
|
+
|
|
149
|
+
- **Nothing drawn can overlap it.** A child X window is stacked above
|
|
150
|
+
everything painted in its parent. An overlay — a HUD, a "reconnecting…"
|
|
151
|
+
banner — belongs in a sibling `<popup>`. `<foreign>` takes no children and
|
|
152
|
+
says so rather than laying out something that could never be seen.
|
|
153
|
+
- **Pointer events over it are the client's.** They never reach react-x11's
|
|
154
|
+
hit testing, which is exactly right for a terminal or a video surface.
|
|
155
|
+
|
|
156
|
+
`backgroundColor` in the `style` is what shows in the rect before a client
|
|
157
|
+
arrives and after one leaves — it is the container window's background, painted
|
|
158
|
+
by the server. There is no separate prop for it, so the name is the one used
|
|
159
|
+
everywhere else.
|
|
160
|
+
|
|
161
|
+
## Focus, and who gets the keys
|
|
162
|
+
|
|
163
|
+
X gives the input focus to one window. XEmbed's answer is that the embedder
|
|
164
|
+
holds the real focus and gives the client a _logical_ one by message, and the
|
|
165
|
+
classic implementation of that is a **focus proxy**: an invisible InputOnly
|
|
166
|
+
window that takes the X focus and forwards keystrokes into the client with
|
|
167
|
+
`SendEvent`.
|
|
168
|
+
|
|
169
|
+
**react-x11 does not use a proxy**, and the reason matters if you are reading
|
|
170
|
+
the code expecting one:
|
|
171
|
+
|
|
172
|
+
- A proxy inside our own toplevel means the toplevel sees `FocusOut` the moment
|
|
173
|
+
the client is focused. react-x11 reads that as the window losing focus —
|
|
174
|
+
carets stop blinking, `<window onBlur>` fires — and the element would be told
|
|
175
|
+
to blur the client it had just focused.
|
|
176
|
+
- A key that reaches a proxy has already gone past every handler in the React
|
|
177
|
+
tree, so an application chord could never be checked first.
|
|
178
|
+
|
|
179
|
+
So the X focus stays on your window, and the rule is:
|
|
180
|
+
|
|
181
|
+
> While a `<foreign>` holds focus, the application's handlers see every key
|
|
182
|
+
> first. Anything they do not consume is forwarded to the client.
|
|
183
|
+
|
|
184
|
+
`preventDefault()` is how they consume it — the same word, and the same
|
|
185
|
+
ordering, that lets a `<textinput>` keep Tab as an indent key:
|
|
186
|
+
|
|
187
|
+
```jsx
|
|
188
|
+
<window
|
|
189
|
+
onKeyDown={(ev) => {
|
|
190
|
+
if (ev.ctrlKey && ev.keysym === XK_t) {
|
|
191
|
+
ev.preventDefault(); // the app's chord; the terminal never sees it
|
|
192
|
+
newTab();
|
|
193
|
+
}
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<foreign windowId={id} style={{ flexGrow: 1 }} />
|
|
197
|
+
</window>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**A menu accelerator is not a handler**, and this is the one place that shows.
|
|
201
|
+
[Accelerators](events.md#accelerators) run _after_ the focused element's own
|
|
202
|
+
default action, and `<foreign>`'s is to forward the key and consume it — so
|
|
203
|
+
while an embedded terminal has focus its Ctrl+C is the terminal's, not the
|
|
204
|
+
Edit menu's, which is what a user of that terminal expects. An application
|
|
205
|
+
chord that must win everywhere goes in an `onKeyDown` like the one above,
|
|
206
|
+
which is checked before either.
|
|
207
|
+
|
|
208
|
+
Everything else about focus is the ordinary focus manager:
|
|
209
|
+
|
|
210
|
+
| what happens | what the client is told |
|
|
211
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| the node takes focus by click or `focus()` | `XEMBED_WINDOW_ACTIVATE`, then `XEMBED_FOCUS_IN` detail `CURRENT` |
|
|
213
|
+
| Tab into it | `FOCUS_IN` detail `FIRST` — focus your first widget |
|
|
214
|
+
| Shift+Tab into it | `FOCUS_IN` detail `LAST` |
|
|
215
|
+
| focus leaves, or the window deactivates | `FOCUS_OUT`, then `WINDOW_DEACTIVATE` |
|
|
216
|
+
| the client sends `XEMBED_REQUEST_FOCUS` | the node is focused _through the focus manager_, so `:focus`, `:focus-within`, the previous node's `onBlur` and the AT-SPI bridge all see it |
|
|
217
|
+
| the client sends `FOCUS_NEXT` / `FOCUS_PREV` | focus moves to the next/previous react-x11 focusable — Tab crosses the boundary as if it were one application |
|
|
218
|
+
|
|
219
|
+
A `<foreign>` is focusable by default, like a `<textinput>`. `focusable={false}`
|
|
220
|
+
opts out, which is what a video surface wants.
|
|
221
|
+
|
|
222
|
+
Tab is _forwarded_ rather than acted on: an XEmbed client has a tab chain of its
|
|
223
|
+
own and says so with `FOCUS_NEXT` when it runs off the end of it. A client that
|
|
224
|
+
speaks no XEmbed — a terminal — keeps Tab for good, which is what a terminal is
|
|
225
|
+
for; click elsewhere to leave.
|
|
226
|
+
|
|
227
|
+
**Dead keys and Compose stay out of it.** react-x11's own composition
|
|
228
|
+
([events.md](events.md#composition)) does not run while a `<foreign>` holds
|
|
229
|
+
focus: the client is another program with an input method of its own, and an
|
|
230
|
+
accent composed on this side would swallow the `KeyPress` on its way there
|
|
231
|
+
and hand back a character with nowhere to put it. The raw key is forwarded
|
|
232
|
+
and the client composes it, which is also what makes `é` work in an embedded
|
|
233
|
+
terminal whatever the toolkit inside it is.
|
|
234
|
+
|
|
235
|
+
### The one gap
|
|
236
|
+
|
|
237
|
+
X delivers a key to the deepest descendant of the focus window that contains the
|
|
238
|
+
pointer. So while the pointer is _over_ the embedded client, the client receives
|
|
239
|
+
keys directly and no handler of yours runs — application chords do not work
|
|
240
|
+
there. They work everywhere else in the window, which is the trade a focus proxy
|
|
241
|
+
would make in the other direction.
|
|
242
|
+
|
|
243
|
+
## Reparenting window managers
|
|
244
|
+
|
|
245
|
+
A `<foreign>` inside a toplevel that the window manager has reparented into a
|
|
246
|
+
frame is offset by that frame, and the root-relative coordinates in the
|
|
247
|
+
synthetic `ConfigureNotify` have to account for it. react-x11 tracks the
|
|
248
|
+
toplevel's screen origin from `ReparentNotify`/`ConfigureNotify` and asks the
|
|
249
|
+
server rather than trusting reported geometry — the same problem
|
|
250
|
+
`src/components/anchor.js` solves for popup placement.
|
|
251
|
+
|
|
252
|
+
## Props
|
|
253
|
+
|
|
254
|
+
| prop | |
|
|
255
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `windowId` | the X window to embed. Changing it hands the old client back first. Omit it to adopt whatever is put inside the node instead. |
|
|
257
|
+
| `onReady` | `({ windowId })` — the node's own container id, offered before anything is embedded, to spawn a program into |
|
|
258
|
+
| `onEmbedded` | `({ id, xembed, version })` — a client is in |
|
|
259
|
+
| `onClientGone` | destroyed, or reparented away by someone else |
|
|
260
|
+
| `onRequestFocus` | the client asked for the focus (it is then given through the focus manager) |
|
|
261
|
+
| `onError` | the embed failed — no such window, or it went away mid-handshake. Without a handler, a console warning. |
|
|
262
|
+
| `focusable` | default `true`; takes part in the focus manager and the tab chain |
|
|
263
|
+
| `style` | layout as usual; `backgroundColor` is what shows with no client in |
|
|
264
|
+
|
|
265
|
+
## Example
|
|
266
|
+
|
|
267
|
+
`examples/foreign.jsx` spawns a program into a pane and then lets go of it,
|
|
268
|
+
which is the whole element in one click: the pane disappears and the terminal
|
|
269
|
+
is still there, framed by the window manager, as if it had started on its own.
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
npm run examples:foreign # xterm, if you have it
|
|
273
|
+
FOREIGN_CMD="mpv --wid=%WID% video.mp4" npm run examples:foreign
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## What is not here yet
|
|
277
|
+
|
|
278
|
+
- **`<TrayHost>`** — owning the `_NET_SYSTEM_TRAY_S<screen>` manager selection
|
|
279
|
+
and rendering a `<foreign>` per docked icon, which is what makes a react-x11
|
|
280
|
+
panel a real desktop shell. The system tray is XEmbed's biggest surviving
|
|
281
|
+
consumer.
|
|
282
|
+
- **The plug side** — a react-x11 root that renders _into_ someone else's
|
|
283
|
+
socket, for shipping as a tray icon or a panel applet. Lower value and
|
|
284
|
+
deliberately last: GTK4 removed `GtkPlug` and Qt dropped `QX11Embed*` after
|
|
285
|
+
Qt4, so the population of hosts willing to embed us is shrinking while the
|
|
286
|
+
population of clients we can host is not.
|
|
287
|
+
|
|
288
|
+
Both are tracked in [issue #269](https://github.com/sidorares/react-x11/issues/269).
|