react-x11 2.51.1 → 2.53.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 +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- 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/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -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/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Custom URI schemes and single instances
|
|
2
|
+
|
|
3
|
+
Being the app that `com.example.myapp://…` opens: a link clicked in a browser,
|
|
4
|
+
a chat message or a launcher reaches the **already-running** app, and its
|
|
5
|
+
window comes to the front.
|
|
6
|
+
|
|
7
|
+
The flow this exists for is logging in through the browser. The app opens the
|
|
8
|
+
system browser, the provider redirects to
|
|
9
|
+
`com.example.myapp://auth?code=…`, the desktop routes that URI back, and the
|
|
10
|
+
app finishes the login without asking anyone to paste anything.
|
|
11
|
+
|
|
12
|
+
## Read this first: for a login, you probably want a loopback port
|
|
13
|
+
|
|
14
|
+
RFC 8252 §7.3 has a second answer that needs none of the machinery below —
|
|
15
|
+
redirect to `http://127.0.0.1:<port>/cb` and listen on it:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { createServer } from 'node:http';
|
|
19
|
+
|
|
20
|
+
const server = createServer((req, res) => {
|
|
21
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
22
|
+
res.end('You can close this tab.');
|
|
23
|
+
finishLogin(url.searchParams.get('code'));
|
|
24
|
+
});
|
|
25
|
+
server.listen(0); // "Authorization servers MUST allow any port" — RFC 8252 §7.3
|
|
26
|
+
const port = server.address().port;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No desktop registration, no D-Bus, no install step, and it works on XQuartz,
|
|
30
|
+
in a container and over ssh where a scheme handler does not. **For an OAuth
|
|
31
|
+
flow, make this the default and use the rest of this page only when you
|
|
32
|
+
cannot.**
|
|
33
|
+
|
|
34
|
+
Custom schemes are still needed for providers that only accept a registered
|
|
35
|
+
private-use scheme, and for deep links that are not logins
|
|
36
|
+
(`myapp://project/42` from a chat message, a document opened from a launcher).
|
|
37
|
+
|
|
38
|
+
## Four things have to be true
|
|
39
|
+
|
|
40
|
+
| | | where |
|
|
41
|
+
| ---------------- | ---------------------------------------------------------- | ----------------------- |
|
|
42
|
+
| **Registration** | a `.desktop` entry claiming `x-scheme-handler/…` | an install step |
|
|
43
|
+
| **Dispatch** | the URI reaching the running process | `registerApplication()` |
|
|
44
|
+
| **Delivery** | `org.freedesktop.Application.Open`, answered before the UI | `useAppOpen()` |
|
|
45
|
+
| **The raise** | `_NET_ACTIVE_WINDOW`, with the right timestamp | `activateWindow()` |
|
|
46
|
+
|
|
47
|
+
The raise is the one that decides whether a user calls the feature working;
|
|
48
|
+
see [the raise is the part that fails](#the-raise-is-the-part-that-fails).
|
|
49
|
+
|
|
50
|
+
## The code
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import { createRoot, registerApplication } from 'react-x11';
|
|
54
|
+
|
|
55
|
+
const app = await registerApplication({
|
|
56
|
+
appId: 'com.example.myapp',
|
|
57
|
+
schemes: ['com.example.myapp'],
|
|
58
|
+
});
|
|
59
|
+
if (app?.role === 'secondary') process.exit(0); // we do not exit for you
|
|
60
|
+
|
|
61
|
+
const root = await createRoot();
|
|
62
|
+
root.render(<App />);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```jsx
|
|
66
|
+
import { activateWindow, useAppOpen } from 'react-x11';
|
|
67
|
+
|
|
68
|
+
function App() {
|
|
69
|
+
const win = useRef(null);
|
|
70
|
+
useAppOpen((uris, ctx) => {
|
|
71
|
+
activateWindow(win, { timestamp: ctx.timestamp });
|
|
72
|
+
const code = new URL(uris[0]).searchParams.get('code');
|
|
73
|
+
finishLogin(code);
|
|
74
|
+
});
|
|
75
|
+
return <window ref={win}>…</window>;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That is the whole runtime half.
|
|
80
|
+
|
|
81
|
+
### `registerApplication()`
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
function registerApplication(options: {
|
|
85
|
+
appId: string;
|
|
86
|
+
schemes?: string[];
|
|
87
|
+
onOpen?: (uris: string[], ctx: LaunchContext) => void;
|
|
88
|
+
onActivate?: (ctx: LaunchContext) => void;
|
|
89
|
+
onAction?: (name: string, params: unknown[], ctx: LaunchContext) => void;
|
|
90
|
+
argv?: string[];
|
|
91
|
+
}): Promise<AppRegistration | null>;
|
|
92
|
+
|
|
93
|
+
interface AppRegistration {
|
|
94
|
+
readonly role: 'primary' | 'secondary';
|
|
95
|
+
readonly appId: string;
|
|
96
|
+
readonly objectPath: string;
|
|
97
|
+
release(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Call it before `createRoot()`.** On the D-Bus path the bus _started this
|
|
102
|
+
process_ because someone called `Open`, and that call is outstanding while the
|
|
103
|
+
app boots — registering from inside a component answers it with an
|
|
104
|
+
unknown-method error, and the launch is lost.
|
|
105
|
+
|
|
106
|
+
**`appId` is not a free parameter.** The desktop entry spec ties the
|
|
107
|
+
well-known bus name, the `.desktop` file's basename and (RFC 8252 §7.1) the URI
|
|
108
|
+
scheme to one string. The object path is derived from it: dots become slashes,
|
|
109
|
+
a slash goes on the front, and **a dash becomes an underscore**
|
|
110
|
+
(`com.example.my-app` is served at `/com/example/my_app`).
|
|
111
|
+
|
|
112
|
+
**`null` means there is no session bus** — ssh, a bare `startx`, a container,
|
|
113
|
+
CI, Node 20 without the transport. The app runs as an ordinary single-window
|
|
114
|
+
program. A URI that arrived in `argv` is still delivered to `useAppOpen`: a
|
|
115
|
+
cold-start deep link needs no bus, only a second instance does.
|
|
116
|
+
|
|
117
|
+
**A malformed `appId` or an unusable scheme throws**, which is not a hole in
|
|
118
|
+
[the never-throws rule](dbus.md#the-rule-everything-else-is-subordinate-to):
|
|
119
|
+
`null` means "this machine has no bus", and a typo must not hide behind it.
|
|
120
|
+
|
|
121
|
+
### Two dispatch paths, and why `role` exists
|
|
122
|
+
|
|
123
|
+
Openers that honour `DBusActivatable=true` (GIO: `gio open`, most of GNOME)
|
|
124
|
+
call `Open` on the running app. `xdg-open`'s generic fallback — which Chromium
|
|
125
|
+
uses, and which every WM-only session gets — ignores it and runs the `Exec`
|
|
126
|
+
line, so **a second copy of your app is spawned with the URI in `argv`**.
|
|
127
|
+
`registerApplication` handles both: the second copy forwards the URI to the
|
|
128
|
+
first and reports its role.
|
|
129
|
+
|
|
130
|
+
| `role` | |
|
|
131
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
132
|
+
| `'primary'` | this process owns the name and will receive `Open` |
|
|
133
|
+
| `'secondary'` | another instance owns it; this launch was forwarded — **exit now** |
|
|
134
|
+
|
|
135
|
+
It does not call `process.exit()` for you; exiting is the app's decision.
|
|
136
|
+
|
|
137
|
+
A second launch with **no** URI forwards `Activate` rather than an empty
|
|
138
|
+
`Open`, so "the user started the app again" and "the user clicked a link"
|
|
139
|
+
arrive as different events.
|
|
140
|
+
|
|
141
|
+
### Delivery: buffered, and replayed
|
|
142
|
+
|
|
143
|
+
`Open` is answered on the turn it arrives — never when the UI is ready. The
|
|
144
|
+
URIs are buffered and replayed to the **first handler that attaches**, so an
|
|
145
|
+
app whose React tree mounts a second later loses nothing, and an app whose
|
|
146
|
+
first frame is slow does not creep towards the caller's 25-second timeout.
|
|
147
|
+
|
|
148
|
+
Replay happens once. A second subscriber does not get the same login again.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
function useAppOpen(
|
|
152
|
+
handler: (uris: string[], ctx: LaunchContext) => void,
|
|
153
|
+
): void;
|
|
154
|
+
function useAppActivate(handler: (ctx: LaunchContext) => void): void;
|
|
155
|
+
|
|
156
|
+
// the same two, for code that has no component to hang off
|
|
157
|
+
function onAppOpen(handler): () => void;
|
|
158
|
+
function onAppActivate(handler): () => void;
|
|
159
|
+
|
|
160
|
+
interface LaunchContext {
|
|
161
|
+
readonly platformData: Readonly<Record<string, unknown>>;
|
|
162
|
+
readonly timestamp: number | null;
|
|
163
|
+
readonly startupId: string | null;
|
|
164
|
+
readonly activationToken: string | null;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
There is **nothing to wrap** — no provider, no prop drilling. The registration
|
|
169
|
+
is process-global, so a component library can call these hooks.
|
|
170
|
+
|
|
171
|
+
`schemes` filters what arrives: a URI whose scheme you did not register is
|
|
172
|
+
dropped before your handler sees it. `file:` is always allowed alongside them,
|
|
173
|
+
because `Open` is also how a file manager says "open this document with you".
|
|
174
|
+
Declaring no schemes at all filters nothing.
|
|
175
|
+
|
|
176
|
+
### The raise is the part that fails
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
function activateWindow(
|
|
180
|
+
target?: NtkWindow | DrawnNode | RefObject<…> | number | null,
|
|
181
|
+
options?: { timestamp?: number | null; source?: 1 | 2 },
|
|
182
|
+
): boolean;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Sends `_NET_ACTIVE_WINDOW` to the root window: source indication, timestamp,
|
|
186
|
+
and this app's currently active toplevel. `target` is anything
|
|
187
|
+
[`windowIdOf()`](elements.md) accepts, or nothing — in which case this app's
|
|
188
|
+
only top-level window is used, its focused one when it has several.
|
|
189
|
+
|
|
190
|
+
**The timestamp is the whole feature.** A window manager's focus-stealing
|
|
191
|
+
prevention weighs the X timestamp of the user action that triggered the launch
|
|
192
|
+
(the digits after `_TIME` in the startup id). Get it wrong and nothing reports
|
|
193
|
+
an error: the app receives the code, the WM declines to raise it, and the
|
|
194
|
+
taskbar entry blinks.
|
|
195
|
+
|
|
196
|
+
| `timestamp` | |
|
|
197
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| omitted | the last input this app saw — EWMH's own definition of the field |
|
|
199
|
+
| `ctx.timestamp` | **what a deep link should pass**: the click in the browser is the user action, not whatever this app last saw |
|
|
200
|
+
| `null` | `CurrentTime`; legal, and most window managers will decline it |
|
|
201
|
+
|
|
202
|
+
The return value is whether the request was **issued**, not whether it worked:
|
|
203
|
+
the window manager may refuse and set `_NET_WM_STATE_DEMANDS_ATTENTION`
|
|
204
|
+
instead (the blinking taskbar entry), and no client can tell.
|
|
205
|
+
|
|
206
|
+
## Registration: the half that is not runtime code
|
|
207
|
+
|
|
208
|
+
react-x11 does **not** write into `~/.local/share` for you (that would not
|
|
209
|
+
work for a system-installed or Flatpak app anyway). Ship these from your
|
|
210
|
+
installer, your package, or a setup command your users run.
|
|
211
|
+
|
|
212
|
+
`~/.local/share/applications/com.example.myapp.desktop` — the basename is tied
|
|
213
|
+
to the bus name by the spec:
|
|
214
|
+
|
|
215
|
+
```ini
|
|
216
|
+
[Desktop Entry]
|
|
217
|
+
Type=Application
|
|
218
|
+
Name=My App
|
|
219
|
+
Exec=my-app %u
|
|
220
|
+
Icon=com.example.myapp
|
|
221
|
+
DBusActivatable=true
|
|
222
|
+
StartupWMClass=com.example.myapp
|
|
223
|
+
MimeType=x-scheme-handler/com.example.myapp;
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`StartupWMClass` must match the `appId` prop on your `<window>`, or the
|
|
227
|
+
window manager cannot match the window that appears to the launch that asked
|
|
228
|
+
for it.
|
|
229
|
+
|
|
230
|
+
Then tell the MIME database it changed:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
update-desktop-database ~/.local/share/applications
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
And, so that the bus can _start_ the app when it is not running,
|
|
237
|
+
`~/.local/share/dbus-1/services/com.example.myapp.service`:
|
|
238
|
+
|
|
239
|
+
```ini
|
|
240
|
+
[D-BUS Service]
|
|
241
|
+
Name=com.example.myapp
|
|
242
|
+
Exec=/usr/bin/my-app
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Checking it by hand
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
gio open com.example.myapp://test # path A: D-Bus activation
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
env -u XDG_CURRENT_DESKTOP xdg-open com.example.myapp://test # path B: Exec %u
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Run each twice: once with the app closed (it should start and handle the link)
|
|
256
|
+
and once with it running (it should come to the front, and no second window
|
|
257
|
+
should appear). `REACT_X11_DEBUG_URI=1` narrates what this module decided —
|
|
258
|
+
with every URI's query and fragment stripped, so the output is safe to paste
|
|
259
|
+
into an issue.
|
|
260
|
+
|
|
261
|
+
## Security
|
|
262
|
+
|
|
263
|
+
The URI is attacker-controlled input from an unauthenticated local caller.
|
|
264
|
+
|
|
265
|
+
- **Scheme squatting is unfixable and expected.** Nothing in the MIME database
|
|
266
|
+
enforces uniqueness; the last handler to register wins. RFC 8252 §6 requires
|
|
267
|
+
PKCE for public native clients — **use it**, and do not treat receiving a
|
|
268
|
+
code as proof of anything. The reverse-domain naming rule (why `appId` is
|
|
269
|
+
validated) is the other half of the mitigation.
|
|
270
|
+
- **`Open` is callable by any peer on the session bus.** It is not an
|
|
271
|
+
authenticated entry point: the bus is a per-user socket, not an
|
|
272
|
+
authorization check. Validate what you receive, and never turn a URI's path
|
|
273
|
+
into a filesystem path.
|
|
274
|
+
- **`platform_data` is spoofable.** A hostile local process can fabricate a
|
|
275
|
+
`desktop-startup-id` and make your app raise itself. Same uid, so it is focus
|
|
276
|
+
theft rather than data loss — but treat `ctx.timestamp` as a hint, not a
|
|
277
|
+
credential.
|
|
278
|
+
- **On the `xdg-open` path the URI lands in `argv`**, so a code is briefly
|
|
279
|
+
visible in `/proc/<pid>/cmdline` to processes of the same user. The D-Bus
|
|
280
|
+
path does not have this property; loopback avoids it too.
|
|
281
|
+
- **Never log the URI.** react-x11 does not: everything on
|
|
282
|
+
`REACT_X11_DEBUG_URI` goes through a redaction that drops the query, the
|
|
283
|
+
fragment and any userinfo. Hold your own logging to the same rule — the code
|
|
284
|
+
is _in_ the URI.
|
|
285
|
+
|
|
286
|
+
## On macOS
|
|
287
|
+
|
|
288
|
+
The same hooks, a different transport. On the cocoa backend the OS talks to
|
|
289
|
+
the application through **Apple Events**, which `@windowkit/appkit` (≥ 0.5)
|
|
290
|
+
forwards into the same delivery as a D-Bus `Open`:
|
|
291
|
+
|
|
292
|
+
| | |
|
|
293
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
294
|
+
| `kAEGetURL` — a URL for a scheme the bundle claims | `useAppOpen(uris, ctx)`, filtered by the registration's `schemes` like an `Open` |
|
|
295
|
+
| `kAEOpenDocuments` — a document from the Finder | `useAppOpen`, as `file://` URLs (`file:` is always allowed) |
|
|
296
|
+
| `kAEReopenApplication` — a Dock click, `open -a` | `useAppActivate(ctx)`, with `ctx.platformData['has-visible-windows']` |
|
|
297
|
+
| `kAEQuitApplication` — Dock › Quit, ⌘Q, a logout | the **primary window's close request**: `onCloseRequest` where there is one, else the default, which closes the app |
|
|
298
|
+
|
|
299
|
+
Three things differ from the bus, and none of them changes app code:
|
|
300
|
+
|
|
301
|
+
- **Single instance is the platform default.** Launch Services activates the
|
|
302
|
+
running app rather than starting a second one, so there is no `secondary`
|
|
303
|
+
role to exit from; `registerApplication()` resolves `null` on a Mac with no
|
|
304
|
+
D-Bus (the normal Mac) and its `schemes` still filter what arrives.
|
|
305
|
+
- **`ctx.timestamp` is `null`.** Apple Events carry no startup id, and the
|
|
306
|
+
raise does not need one: `activateWindow()` there is `NSApp.activate`, which
|
|
307
|
+
no focus-stealing rule weighs.
|
|
308
|
+
- **Registration is `Info.plist`, not a `.desktop` file** — `CFBundleURLTypes`
|
|
309
|
+
for the scheme and `CFBundleDocumentTypes` for documents, in an app bundle
|
|
310
|
+
(see [packaging.md](packaging.md)). A bare `node` process receives nothing
|
|
311
|
+
from Launch Services, because there is no bundle for it to look up.
|
|
312
|
+
|
|
313
|
+
A quit becomes the same close request the red light sends to the app's primary
|
|
314
|
+
window: an `onCloseRequest` that opens a "save your work?" dialog has vetoed it
|
|
315
|
+
until it decides, and one that unmounts has accepted it. Once the app has
|
|
316
|
+
closed on a quit, the process ends
|
|
317
|
+
(`createRoot({ cocoa: { exitOnQuit: false } })` turns that off for an embedder
|
|
318
|
+
that owns the process). One limit: a **logout** is cancelled by macOS the
|
|
319
|
+
moment it asks, and the app then quits on its own — the logout has to be
|
|
320
|
+
started again.
|
|
321
|
+
|
|
322
|
+
## Over ssh
|
|
323
|
+
|
|
324
|
+
Both mechanisms only work when the browser runs on the **same host as the
|
|
325
|
+
app**. A URL pasted into your laptop's browser reaches neither a loopback port
|
|
326
|
+
nor a scheme handler on the server you are `ssh -X`'d into.
|
|
327
|
+
|
|
328
|
+
## Deliberately not here
|
|
329
|
+
|
|
330
|
+
- **Desktop actions** (jump-list entries): `ActivateAction` replies correctly
|
|
331
|
+
and `onAction` receives it; nothing more.
|
|
332
|
+
- **Writing `.desktop` files** — your installer's job (see above).
|
|
333
|
+
- **A full OAuth client** — no PKCE implementation or token store.
|
|
334
|
+
- **`http`/`https` handling** — being the default browser is not this.
|
|
335
|
+
- **The launcher's Wayland `xdg-activation` token** is exposed on
|
|
336
|
+
`ctx.activationToken` and otherwise ignored; on the
|
|
337
|
+
[Wayland backend](wayland-backend.md) `activateWindow()` requests a token of
|
|
338
|
+
its own.
|
|
339
|
+
|
|
340
|
+
Design notes: [design/uri-schemes.md](../design/uri-schemes.md).
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# The Wayland backend
|
|
2
|
+
|
|
3
|
+
**Status: a working backend, opt-in.** react-x11's own reconciler renders a
|
|
4
|
+
window on a real Wayland compositor (mutter/GNOME, sway, labwc) with
|
|
5
|
+
decorations — the compositor's where it draws them, client-side where it
|
|
6
|
+
does not — text, shadows, paths, images, live partial repaints, pointer,
|
|
7
|
+
touch, pen and keyboard input decoded against the compositor's keymap, an
|
|
8
|
+
input method, a clipboard, drag and drop, popups, layer-shell docks and
|
|
9
|
+
panels, and a screen capture with an eyedropper on top — drawn on the GPU
|
|
10
|
+
and handed over as dma-buf, on **Node or Bun**, with no X server and no
|
|
11
|
+
Xwayland anywhere in the path.
|
|
12
|
+
|
|
13
|
+
It implements the research RFC in [design/wayland.md](../design/wayland.md);
|
|
14
|
+
where the two disagree, this page is what ran. How it is built, what was
|
|
15
|
+
measured and what is left are in the
|
|
16
|
+
[design notes](../design/wayland-backend.md).
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
REACT_X11_BACKEND=wayland node your-app.mjs # x11-dri >= 0.9 (native socket)
|
|
20
|
+
REACT_X11_BACKEND=wayland bun your-app.mjs # or Bun's bun:ffi transport
|
|
21
|
+
node examples/wayland-gl.mjs # GPU plasma, paced by the compositor
|
|
22
|
+
node examples/wayland-ui.mjs # every drawing primitive, no React
|
|
23
|
+
node --import tsx --test 'test/wayland/*.test.js' # the suite: no display needed
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The two packages it needs beyond react-x11's own are optional dependencies,
|
|
27
|
+
installed with it: [`@windowkit/wayland`](https://github.com/windowkit/wayland),
|
|
28
|
+
the protocol layer — a fork of `wayland-client` that passes file descriptors
|
|
29
|
+
— and [`x11-dri`](https://github.com/sidorares/node-x11-dri) 0.9 or later,
|
|
30
|
+
the GPU context and, on Node, the socket: Wayland passes file descriptors
|
|
31
|
+
(the keymap, shm pools, clipboard pipes) and Node's own sockets cannot. Under
|
|
32
|
+
Bun the socket is node-x11's `bun:ffi` transport instead.
|
|
33
|
+
|
|
34
|
+
The backend is never chosen by `'auto'`: X11 stays the default on Linux
|
|
35
|
+
because the remote case ([remote.md](remote.md)) is the flagship reason this
|
|
36
|
+
project exists and Wayland has no network transparency.
|
|
37
|
+
|
|
38
|
+
## How to tell it is not Xwayland
|
|
39
|
+
|
|
40
|
+
A GNOME session answers `$DISPLAY` too, through Xwayland, so "the window
|
|
41
|
+
appeared" proves nothing. Three checks that do:
|
|
42
|
+
|
|
43
|
+
- **Unset `DISPLAY`.** The Wayland backend never opens it; the X11 backend
|
|
44
|
+
cannot start without it.
|
|
45
|
+
```bash
|
|
46
|
+
env -u DISPLAY REACT_X11_BACKEND=wayland node app.mjs # works only natively
|
|
47
|
+
```
|
|
48
|
+
- **Ask the app.** `root.app.backend === 'wayland'`, and `root.app.X` is a
|
|
49
|
+
stand-in — no `display.screen`, no `X.require`. The examples print the
|
|
50
|
+
transport they connected through (`connected via x11-dri` or
|
|
51
|
+
`fdpass-bun`) and never import an X module at all.
|
|
52
|
+
- **Ask the desktop.** `xlsclients` and `xwininfo -root -tree` list Xwayland
|
|
53
|
+
clients; a native window is absent from both. GNOME's Looking Glass
|
|
54
|
+
(`Alt+F2`, `lg`, Windows tab) shows it as a `MetaWindowWayland`, and its
|
|
55
|
+
frame is this backend's own titlebar rather than mutter's Adwaita frame,
|
|
56
|
+
which only Xwayland windows get.
|
|
57
|
+
|
|
58
|
+
## What runs
|
|
59
|
+
|
|
60
|
+
| piece | what an app gets |
|
|
61
|
+
| ------------------ | --------------------------------------------------------------------------------------- |
|
|
62
|
+
| windows and popups | xdg toplevels and popups, fractional scale ([below](#windows-what-maps-what-bends)) |
|
|
63
|
+
| 2d context | rects, rounded rects, paths, exact clips, gradients, patterns, images, text, glyph runs |
|
|
64
|
+
| `<glarea>` | GLES drawing into a rectangle of the window, its children composited over it |
|
|
65
|
+
| offscreen surfaces | the paint cache and scroll blits; a held `Surface` context draws at any time |
|
|
66
|
+
| input | pointer, keyboard decoded against the compositor's keymap, key repeat |
|
|
67
|
+
| touch and tablet | fingers and pens drive the pointer, with pen fields; raw touches |
|
|
68
|
+
| input methods | the compositor's IME (`zwp_text_input_v3`) into the composition events |
|
|
69
|
+
| decorations | the compositor's frame where it offers one, client-side otherwise |
|
|
70
|
+
| layer surfaces | docks, panels, wallpapers, overlays — wlr-layer-shell |
|
|
71
|
+
| screen capture | wlr-screencopy / ext-image-copy-capture; the eyedropper |
|
|
72
|
+
| clipboard | clipboard and primary selection |
|
|
73
|
+
| drag and drop | both directions, in-app drops keep `e.items` by reference; no drag icon yet |
|
|
74
|
+
| screens, keyboard | `useScreens()` and `useKeyboardState()` ([system.md](system.md#on-wayland)) |
|
|
75
|
+
| tests | `test/wayland/`: an in-process compositor, no display needed |
|
|
76
|
+
|
|
77
|
+
The keymap is the compositor's own, so AltGr and levels 3 and 4 work
|
|
78
|
+
because the keymap says which modifier they are, not because one is
|
|
79
|
+
assumed. Compositors do not repeat keys; the backend repeats them at the
|
|
80
|
+
seat's advertised rate.
|
|
81
|
+
|
|
82
|
+
**Touch and pen.** A finger or a tablet tool drives the same mouse events
|
|
83
|
+
as a mouse, and only those events carry `pointerType` (`'touch'`, `'pen'`,
|
|
84
|
+
`'eraser'`, `'mouse'`), `pressure`, `tiltX`/`tiltY`, `rotation`,
|
|
85
|
+
`distance` and `tangentialPressure`, in the DOM's names and ranges
|
|
86
|
+
([events.md](events.md#event-object)); a mouse event is unchanged. Only
|
|
87
|
+
the first finger of a gesture emulates the pointer. Every finger also
|
|
88
|
+
arrives raw on the window as `touchstart`/`touchmove`/`touchend`/
|
|
89
|
+
`touchcancel` (the X11 backend's XI2 names, with `touchId`), each with the
|
|
90
|
+
active `touches`, batched per touch frame. Barrel buttons map as the
|
|
91
|
+
desktop does — BTN_STYLUS is button 3, BTN_STYLUS2 button 2 — and a tool
|
|
92
|
+
leaving proximity releases whatever it still held.
|
|
93
|
+
|
|
94
|
+
**Input methods.** IBus or whatever the compositor runs reaches
|
|
95
|
+
`<textinput>` and `onCompositionStart`/`Update`/`End` exactly as a dead key
|
|
96
|
+
does. Keys the IME consumes never arrive as key events. A `sensitive`
|
|
97
|
+
field offers the IME no surrounding text and says `sensitive_data`;
|
|
98
|
+
`inputMode` on a `<textinput>` becomes the content purpose (`email`, `url`,
|
|
99
|
+
`tel`, …).
|
|
100
|
+
|
|
101
|
+
**`<glarea>`** is a rectangle of the window's own GPU target, not a child
|
|
102
|
+
surface: the area gets the shared GLES context with `viewport`, `scissor`
|
|
103
|
+
and `bindFramebuffer(null)` re-based on its rect, the tree's 2D is drawn
|
|
104
|
+
under it and the panes its children paint on are blended over it.
|
|
105
|
+
|
|
106
|
+
**Perspective images.** `ctx.drawImageProjected(image, m)` draws an image
|
|
107
|
+
through a 3×3 projection (a plane turned out of the page, as CSS
|
|
108
|
+
`rotateY()` in a `perspective` turns one) as one exact quad. It answers
|
|
109
|
+
`false` and draws nothing where a corner is behind the viewer, for the
|
|
110
|
+
caller to draw the part in front some other way. Only this backend has it
|
|
111
|
+
(XRender on glamor folds the image, CoreGraphics and Direct2D draw through
|
|
112
|
+
an affine matrix alone): test with `'drawImageProjected' in ctx` and tile
|
|
113
|
+
where it is absent.
|
|
114
|
+
|
|
115
|
+
**Glyph runs.** `ctx.drawGlyphs(op, src, positioned)`, for callers that
|
|
116
|
+
place their own glyphs (a terminal grid, a tabular column), is spelled as on
|
|
117
|
+
X11 and Cocoa: the op `ctx.Render.PictOp.Over`, the source
|
|
118
|
+
`ctx.createSolidPicture(r, g, b, a)` — or, as Cocoa also takes, a CSS
|
|
119
|
+
colour string, a premultiplied `[r, g, b, a]`, or `null` for the fill style
|
|
120
|
+
in force.
|
|
121
|
+
|
|
122
|
+
### Running the examples
|
|
123
|
+
|
|
124
|
+
Every example runs on this backend the way it runs on X11, with the display
|
|
125
|
+
taken away so nothing can fall back:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
env -u DISPLAY REACT_X11_BACKEND=wayland bun examples/widgets.jsx # or node --import tsx
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Two are X11 by nature and say so: `examples/wm.jsx` is a window manager and
|
|
132
|
+
`examples/xeyes.jsx` reads the root window's pointer — neither has a
|
|
133
|
+
Wayland counterpart. `examples/viewer3d.jsx` draws its model here through
|
|
134
|
+
its direct half, GLES on the window's own GPU target — the same half X11
|
|
135
|
+
uses under `NTK_GL_POLICY=direct`.
|
|
136
|
+
|
|
137
|
+
Environment variables, for desktops with no screenshot API and for
|
|
138
|
+
debugging:
|
|
139
|
+
|
|
140
|
+
- `REACT_X11_WAYLAND_TRACE=1` writes a line per presented frame.
|
|
141
|
+
- `REACT_X11_WAYLAND_SNAPSHOT=/tmp/frame.png` writes the backing target of
|
|
142
|
+
the first window at its 30th present (`REACT_X11_WAYLAND_SNAPSHOT_AT`
|
|
143
|
+
changes which) — the pixels the compositor was handed, read back from the
|
|
144
|
+
GPU.
|
|
145
|
+
- `REACT_X11_WAYLAND_FRAME_STALL_MS` is how long a frame callback may go
|
|
146
|
+
unanswered before the window counts as not shown (2000; `0` turns that
|
|
147
|
+
off) — see
|
|
148
|
+
[A surface the compositor stops showing stops drawing](#a-surface-the-compositor-stops-showing-stops-drawing).
|
|
149
|
+
- `REACT_X11_GL_DEVICE` names the DRM node to render on.
|
|
150
|
+
|
|
151
|
+
The GPU context is made on `glPolicy.devicePath`, else on
|
|
152
|
+
`REACT_X11_GL_DEVICE`, else on the first render node, and a device named
|
|
153
|
+
either way is the only one tried. A machine with no render node at all — a
|
|
154
|
+
GitHub-hosted runner, a Hyper-V or VirtualBox guest — usually still has a
|
|
155
|
+
card node Mesa renders on in software, so the backend then tries each
|
|
156
|
+
`/dev/dri/card*` in turn; headless weston with its GL renderer accepts what
|
|
157
|
+
it makes on a hosted runner.
|
|
158
|
+
|
|
159
|
+
## Windows: what maps, what bends
|
|
160
|
+
|
|
161
|
+
The compositor owns placement, stacking and focus, so some of what a
|
|
162
|
+
`<window>` asks for on X11 means less here:
|
|
163
|
+
|
|
164
|
+
- **Position.** A toplevel cannot place itself: `x`/`y` on a `<window>` are
|
|
165
|
+
ignored, not an error, and the compositor's placement is final. A
|
|
166
|
+
toplevel's own position reads `0, 0`; no client knows where it is on
|
|
167
|
+
screen.
|
|
168
|
+
- **Popups** are placed relative to their parent window, and the compositor
|
|
169
|
+
slides and flips them to stay on screen — the policy `anchorRect` gets
|
|
170
|
+
from react-x11 on X11. A press outside a grabbing popup, or Escape, is
|
|
171
|
+
the compositor's to act on; it hides the popup and the tree hears
|
|
172
|
+
`onDismiss` as it would on X11.
|
|
173
|
+
- **Focus and raising** are the compositor's to give. `focus()` asks
|
|
174
|
+
through xdg-activation and is ignored without a recent user interaction;
|
|
175
|
+
attention requests go the same way; `raise()` and `lower()` do nothing.
|
|
176
|
+
- **States.** Maximised, fullscreen and minimised map; there is no
|
|
177
|
+
un-minimise request. Always-on-top, sticky and workspaces have no
|
|
178
|
+
equivalent.
|
|
179
|
+
- **Size.** A window sizes itself, except while the compositor imposes a
|
|
180
|
+
size — maximised, tiled, or mid-resize — when the size it asks for is
|
|
181
|
+
not applied.
|
|
182
|
+
- **Type and class.** `windowType` matters only where layer-shell exists
|
|
183
|
+
([below](#layer-surfaces-docks-panels-wallpapers)); the window's class
|
|
184
|
+
(`app_id`) is fixed at creation.
|
|
185
|
+
- **A window the compositor is not showing does not draw** — next section.
|
|
186
|
+
|
|
187
|
+
## A surface the compositor stops showing stops drawing
|
|
188
|
+
|
|
189
|
+
After the first present a window draws only when the compositor sends it a
|
|
190
|
+
frame callback, and a compositor is entitled never to send one to a
|
|
191
|
+
surface it is not showing — covered, on another workspace, minimised, the
|
|
192
|
+
screen locked. There is no timer-driven fallback, on purpose: painting
|
|
193
|
+
into a surface nobody sees is work for nothing.
|
|
194
|
+
|
|
195
|
+
The silence is **not an error**: nothing rejects, and React goes on
|
|
196
|
+
re-rendering into a window that does not draw, which from outside looks
|
|
197
|
+
like a hang. So the stall is reported twice over:
|
|
198
|
+
|
|
199
|
+
- **On stderr, once per window, in development**, when the window still had
|
|
200
|
+
something to draw. One line, naming the compositor as the reason and saying
|
|
201
|
+
the app is not hung.
|
|
202
|
+
- **In the tree**, as [`useWindowState().presenting`](system.md#presenting-and-the-wayland-frame-clock),
|
|
203
|
+
which also folds into `visible` — so an app can stop animating and
|
|
204
|
+
simulating rather than feeding a loop nobody is turning, which is what
|
|
205
|
+
every map, chart and game on this backend wants anyway.
|
|
206
|
+
|
|
207
|
+
Nothing has to be done to recover: the pending frame callback fires the
|
|
208
|
+
moment the surface is shown again, drawing resumes and `presenting` flips
|
|
209
|
+
back. The deadline is two seconds; `REACT_X11_WAYLAND_FRAME_STALL_MS`
|
|
210
|
+
changes it, and `0` turns the watch off entirely, timer, warning and all.
|
|
211
|
+
|
|
212
|
+
## Decorations: whose frame
|
|
213
|
+
|
|
214
|
+
The backend draws its own titlebar where the compositor will not (mutter),
|
|
215
|
+
and asks for the compositor's frame where `zxdg_decoration_manager_v1` is
|
|
216
|
+
offered — wlroots (sway, labwc), KDE. When the compositor agrees, the
|
|
217
|
+
client-side frame goes away and the content grows into the whole surface,
|
|
218
|
+
which the tree sees as a resize; a window not yet shown comes up the size
|
|
219
|
+
it was declared, not a titlebar taller.
|
|
220
|
+
|
|
221
|
+
`createRoot({ decorations })` says what to ask for: `undefined`/`true`/
|
|
222
|
+
`'server'` prefer the compositor's frame, `'client'` never asks for it, and
|
|
223
|
+
`false` — as does `<window decorations={false}>` — wants no frame at all.
|
|
224
|
+
A frameless window still creates the decoration object where the protocol
|
|
225
|
+
exists, to _decline_ server-side: sway frames every toplevel that has not
|
|
226
|
+
said `client_side`.
|
|
227
|
+
|
|
228
|
+
## Layer surfaces: docks, panels, wallpapers
|
|
229
|
+
|
|
230
|
+
Where the compositor advertises `zwlr_layer_shell_v1`, a `<window>` whose
|
|
231
|
+
`windowType` is one of these is not an `xdg_toplevel` but a layer surface,
|
|
232
|
+
the same `wl_surface`, frame loop and input underneath:
|
|
233
|
+
|
|
234
|
+
| `windowType` | layer | anchored to | exclusive zone |
|
|
235
|
+
| ---------------- | ------------ | ------------------------------------------------- | -------------- |
|
|
236
|
+
| `'dock'` | `top` | the edge its shape and position imply (see below) | its thickness |
|
|
237
|
+
| `'desktop'` | `background` | all four edges | -1 |
|
|
238
|
+
| `'notification'` | `top` | top and right, 12px in | 0 |
|
|
239
|
+
| `'splash'` | `overlay` | nothing — the compositor centres it | 0 |
|
|
240
|
+
|
|
241
|
+
A dock's edge is read the way an X window manager reads the same window:
|
|
242
|
+
wide and at `y={0}` is a top panel, wide otherwise a bottom one; tall and at
|
|
243
|
+
`x={0}` a left dock, tall otherwise a right one. Its size is what the tree
|
|
244
|
+
measured (or `width`/`height`), so a 560×56 dock sits centred on its edge
|
|
245
|
+
with 56px reserved; anchoring it to both ends of the edge stretches it:
|
|
246
|
+
|
|
247
|
+
```jsx
|
|
248
|
+
<window windowType="dock" height={56}>…</window>
|
|
249
|
+
<window windowType="dock" height={40} layerShell={{ anchor: ['bottom', 'left', 'right'] }}>…</window>
|
|
250
|
+
<window layerShell={{ layer: 'overlay', anchor: 'bottom', margin: { bottom: 40 } }}>an OSD</window>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`layerShell` names anything the type's default gets wrong — `layer`,
|
|
254
|
+
`anchor` (an edge, an array of edges, `'all'`, `'none'`), `exclusiveZone`
|
|
255
|
+
(a number, `'auto'` for the thickness, `-1`), `margin`,
|
|
256
|
+
`keyboardInteractivity` (`'none'`, `'on-demand'`, `'exclusive'`),
|
|
257
|
+
`namespace`, `output` — and `layerShell={false}` keeps a dock-typed window
|
|
258
|
+
an ordinary toplevel. Popups from a layer surface work (a dock's menus). A
|
|
259
|
+
stretched axis is the compositor's: `resize()` on it is not granted, and
|
|
260
|
+
the configured size wins. Without layer-shell — GNOME — the same JSX is a
|
|
261
|
+
toplevel.
|
|
262
|
+
|
|
263
|
+
## Screen capture, and the eyedropper
|
|
264
|
+
|
|
265
|
+
`app.screenCapture` is there when the compositor offers either
|
|
266
|
+
`ext_image_copy_capture_manager_v1` (wlroots 0.19+, KDE 6.2+) or
|
|
267
|
+
`zwlr_screencopy_manager_v1` (sway 1.10 has this one), and null otherwise
|
|
268
|
+
(GNOME). `capture({ output, cursor })` resolves to one frame of the output
|
|
269
|
+
as straight RGBA, `{ width, height, data }`; `pixelAt(x, y)` to one device
|
|
270
|
+
pixel of it, as 0–255 channels. Each call is a fresh capture of the whole
|
|
271
|
+
output — for many pixels, `capture()` once; for anything per frame, not at
|
|
272
|
+
all.
|
|
273
|
+
|
|
274
|
+
`pickColor()` is the eyedropper, the way every wlroots colour picker works:
|
|
275
|
+
it freezes the output into a capture, shows it as a full-screen
|
|
276
|
+
layer-shell overlay that takes the keyboard, and reads the pixel under the
|
|
277
|
+
click — so it needs layer-shell as well as a capture protocol. Escape
|
|
278
|
+
cancels; Return, space and KP_Enter pick at the pointer. It answers
|
|
279
|
+
`{ r, g, b }` in 0–1. `useEyedropper()` uses it below the Screenshot portal
|
|
280
|
+
(the desktop's own, user-consented picker — GNOME's only route, and what
|
|
281
|
+
sway with xdg-desktop-portal-wlr uses too), so `useEyedropper().supported`
|
|
282
|
+
is true on a bare sway. Where the compositor has neither the portal's
|
|
283
|
+
picker nor capture plus layer-shell, `supported` is false: leave the
|
|
284
|
+
eyedropper button undrawn.
|
|
285
|
+
|
|
286
|
+
Two limits. With several outputs, the one captured by default is the one
|
|
287
|
+
the compositor advertised last; `capture({ output })` and
|
|
288
|
+
`layerShell.output` take another `wl_output` proxy. And a portal dialog (file chooser, colour picker) is modal
|
|
289
|
+
to its window only where the compositor has **xdg-foreign**; without it, or
|
|
290
|
+
from a popup, the dialog floats.
|
|
291
|
+
|
|
292
|
+
## Under a second compositor
|
|
293
|
+
|
|
294
|
+
The backend runs on sway and labwc as well as mutter, nested
|
|
295
|
+
(`WLR_BACKENDS=wayland`) or headless (`WLR_BACKENDS=headless`). Headless
|
|
296
|
+
sway plus `grim` is a way to screenshot an app with no desktop at all —
|
|
297
|
+
`grim` needs wlr-screencopy, which mutter refuses:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
WLR_BACKENDS=headless sway -c sway.conf & # output * resolution 1280x800
|
|
301
|
+
WAYLAND_DISPLAY=wayland-1 REACT_X11_BACKEND=wayland node --import tsx examples/simple.jsx &
|
|
302
|
+
WAYLAND_DISPLAY=wayland-1 grim shot.png
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Not supported yet
|
|
306
|
+
|
|
307
|
+
- **Multisampling.** Paths filled with a gradient, solid paths larger than
|
|
308
|
+
512×512 after clipping, and clips to anything but one axis-aligned
|
|
309
|
+
rectangle have aliased edges; rounded rectangles, smaller solid paths and
|
|
310
|
+
text are antialiased.
|
|
311
|
+
- **`<Frame>` panes.** A `<Frame>` fails at once: its `fallback` renders and
|
|
312
|
+
no pane is started ([frame.md](frame.md)).
|
|
313
|
+
- **The drag icon.** Drag and drop works both ways, but the drag shows no
|
|
314
|
+
icon: `<popup dragPreview>` is not drawn.
|
|
315
|
+
- **Input methods, the rest of it.** A preedit that hides its cursor puts
|
|
316
|
+
the caret at the end, and the only content hints sent are `multiline`
|
|
317
|
+
and `sensitive_data`.
|
|
318
|
+
- **Tablet pads** — the buttons, rings and strips on the tablet itself are
|
|
319
|
+
ignored.
|
|
320
|
+
- **`useScreens().source` reads `'test'`**, as it does on macOS and Windows.
|
|
321
|
+
- **`fillText`** uses only the first family of a `font` shorthand, and
|
|
322
|
+
strokes text as a fill.
|
|
323
|
+
- **`ext-image-copy-capture`** has not been run against a compositor yet
|
|
324
|
+
(sway 1.10 offers only wlr-screencopy).
|
|
325
|
+
|
|
326
|
+
## Things that are gone, not missing
|
|
327
|
+
|
|
328
|
+
Window managers (`examples/wm.jsx`), global grabs, reading other windows,
|
|
329
|
+
screen coordinates, `GetImage`, XEmbed/`<foreign>`, and `ssh -X` are
|
|
330
|
+
compositor-private by design. Panels and docks are `layer-shell`, and a
|
|
331
|
+
screen read is `screencopy` — both wlroots and KDE, neither GNOME, where the
|
|
332
|
+
same JSX is a toplevel and the same pick is the portal's.
|