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,223 @@
|
|
|
1
|
+
# The eyedropper
|
|
2
|
+
|
|
3
|
+
Sample one pixel from the screen — any window, any application, the
|
|
4
|
+
wallpaper — and get a colour an app can paint with.
|
|
5
|
+
|
|
6
|
+
```jsx
|
|
7
|
+
import { useEyedropper } from 'react-x11';
|
|
8
|
+
|
|
9
|
+
function StrokePicker({ value, onChange }) {
|
|
10
|
+
const eyedropper = useEyedropper();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Button
|
|
14
|
+
label="Pick from screen"
|
|
15
|
+
disabled={eyedropper.picking}
|
|
16
|
+
onPress={async () => {
|
|
17
|
+
const hex = await eyedropper.pick();
|
|
18
|
+
if (hex) onChange(hex); // '#rrggbb'
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Cancelling — Escape, the desktop dialog's own cancel, a dismissed macOS
|
|
26
|
+
sampler — resolves to `null`. It is an ordinary outcome, not an exception,
|
|
27
|
+
on every rung, so the whole error path is one `if`. The same contract as the
|
|
28
|
+
[file dialogs](filedialog.md), on purpose.
|
|
29
|
+
|
|
30
|
+
## The ladder
|
|
31
|
+
|
|
32
|
+
| | | |
|
|
33
|
+
| --- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| 1 | **the system sampler** | `NSColorSampler` on the [cocoa backend](macos.md), where the app the tree renders through carries `colorSampler` (`@windowkit/appkit` >= 0.9). macOS draws the loupe out of process, so the app needs no Screen Recording grant of its own and the user gets the magnifier every other Mac colour picker shows. |
|
|
35
|
+
| 2 | **the portal** | `org.freedesktop.portal.Screenshot.PickColor` over D-Bus. The desktop draws its own magnifier and hands the colour back. GNOME and KDE ship it; it is also GNOME's only route on Wayland. |
|
|
36
|
+
| 3 | **the Wayland picker** | On the [Wayland backend](wayland-backend.md), where the compositor offers screen capture and layer-shell but no portal (wlroots: sway, labwc, …): the output is frozen into a capture, shown as an overlay, and the click is taken on that (`src/wayland/screencopy.js`). Below the portal because the portal is the desktop's own, user-consented picker. |
|
|
37
|
+
| 4 | **X11** | The classic route: grab the pointer with a crosshair, wait for the click, read the 1×1 under it, decode by the server's own pixel layout. Works under a bare WM, over ssh, on XQuartz — everywhere there is a display and nothing else. |
|
|
38
|
+
|
|
39
|
+
`screenColorBackend()` reports which rung this machine lands on without
|
|
40
|
+
grabbing anything; `useEyedropper().supported` is the same answer as render
|
|
41
|
+
state.
|
|
42
|
+
|
|
43
|
+
The first three rungs are the same shape — ask the system, it draws the
|
|
44
|
+
picker, it hands back an sRGB triple — which is why the sampler goes on top
|
|
45
|
+
of the portal and not under the crosshair: where the OS will do this for us,
|
|
46
|
+
it does it better.
|
|
47
|
+
|
|
48
|
+
There is no rung to _draw_, and that is the difference from the file dialog's
|
|
49
|
+
ladder: the thing being read — the whole screen — is precisely what an
|
|
50
|
+
application cannot draw itself. So the hook adds binding, not a fallback of
|
|
51
|
+
its own — and the ladder can therefore run out. Where it does, the floor is
|
|
52
|
+
the typed rejection and `supported: false`, never a crash. A cocoa app on a
|
|
53
|
+
bridge **older than 0.9** is that place: its `app.X` is a stub, so there is
|
|
54
|
+
no pointer to grab and no root window to read either ([macos.md](macos.md)).
|
|
55
|
+
Both system rungs are gated on what they are built out of — one bridge verb,
|
|
56
|
+
three X requests — rather than on there being an app, so a picker there draws
|
|
57
|
+
no eyedropper button instead of one that throws on its first press.
|
|
58
|
+
|
|
59
|
+
### The version gate
|
|
60
|
+
|
|
61
|
+
`PickColor` arrived in **version 2** of the Screenshot interface, and the
|
|
62
|
+
portal being on the bus says nothing about that: `hasService()` sees the
|
|
63
|
+
service, not what its backends provide. XFCE's portal, for example, ships no
|
|
64
|
+
Screenshot interface at all — a machine where every FileChooser call works
|
|
65
|
+
and every PickColor would fail. So the probe reads the interface's own
|
|
66
|
+
`version` property, via `portalVersion()` (exported, since the next portal
|
|
67
|
+
feature will need the same question answered):
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { portalVersion } from 'react-x11';
|
|
71
|
+
|
|
72
|
+
const v = await portalVersion('org.freedesktop.portal.Screenshot');
|
|
73
|
+
// 0 — not there at all; 1 — there, but before PickColor; 2+ — usable
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Where the answer is below 2, the ladder falls through to the X11 rung
|
|
77
|
+
rather than calling a method the portal would refuse.
|
|
78
|
+
|
|
79
|
+
## `useEyedropper()`
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const { pick, supported, picking } = useEyedropper(defaults?);
|
|
83
|
+
|
|
84
|
+
pick(options?): Promise<string | null>; // '#rrggbb', or null on cancel
|
|
85
|
+
supported: boolean; // screenColorBackend() !== null, resolved for you
|
|
86
|
+
picking: boolean; // a pick is in flight — the button's pressed state
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| option | |
|
|
90
|
+
| -------------- | --------------------------------------------------------- |
|
|
91
|
+
| `parentWindow` | override the owner window; inferred otherwise |
|
|
92
|
+
| `signal` | an `AbortSignal` that ends the pick and releases the grab |
|
|
93
|
+
| `backend` | force a rung; the seam for a kiosk, and for tests |
|
|
94
|
+
|
|
95
|
+
Options given to the hook are defaults for every pick; options given to a
|
|
96
|
+
call win. The portal's dialog is parented to the window the component is in,
|
|
97
|
+
resolved when the pick starts — the `useFileDialog()` inference, with
|
|
98
|
+
`parentWindow` as the override for a tree with several top-level windows.
|
|
99
|
+
|
|
100
|
+
Two things the hook does beyond forwarding:
|
|
101
|
+
|
|
102
|
+
- **`picking` is the pressed state**, and it is honest: while it is true, a
|
|
103
|
+
second `pick()` returns the promise already in flight rather than starting
|
|
104
|
+
a second grab — so a double-clicked button cannot make the user click
|
|
105
|
+
twice.
|
|
106
|
+
- **`supported` is almost always true on this toolkit.** An X11 tree has a
|
|
107
|
+
connection by definition, and the connection _is_ the fallback rung; a
|
|
108
|
+
cocoa tree has the system sampler. It goes false when a forced
|
|
109
|
+
`backend: 'portal'` finds no portal, and on a cocoa app whose bridge
|
|
110
|
+
predates the sampler — which is what keeps the button honest on whatever
|
|
111
|
+
platform comes next.
|
|
112
|
+
|
|
113
|
+
## The bare function
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
import { pickScreenColor, screenColorBackend } from 'react-x11';
|
|
117
|
+
|
|
118
|
+
const hex = await pickScreenColor({ app });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Same options, same results. The one addition is `app`: the X11 rung needs a
|
|
122
|
+
connection to grab and read on and the cocoa rung needs the app whose
|
|
123
|
+
sampler to show, and a bare function has no tree to take one from — pass the
|
|
124
|
+
app `createRoot()` returned (or `useApp()` in a component, or a
|
|
125
|
+
`parentWindow` that points at a mounted node, which carries its app with
|
|
126
|
+
it). With no app named, the connections the renderer is currently drawing
|
|
127
|
+
through are asked, the way `openFile()` finds its native panel. Where no
|
|
128
|
+
rung answers it rejects with `NoScreenColorError` — a **typed** rejection,
|
|
129
|
+
so it reads as "hide the button", not as a crash.
|
|
130
|
+
|
|
131
|
+
One pick per connection at a time **on the X11 rung**: a second concurrent
|
|
132
|
+
`pickScreenColor()` on the same app is refused loudly, because a second grab
|
|
133
|
+
from the same client would silently _replace_ the first and one pick would
|
|
134
|
+
settle with the other's click. The hook's shared in-flight promise is the
|
|
135
|
+
friendly version of the same rule. The system sampler needs no such refusal:
|
|
136
|
+
`NSColorSampler` "begins or attaches to an existing color sampling session",
|
|
137
|
+
so a second pick joins the loupe that is up and both callers get the same
|
|
138
|
+
colour.
|
|
139
|
+
|
|
140
|
+
## What differs between the rungs
|
|
141
|
+
|
|
142
|
+
- **The system rungs draw a magnifier; the X11 rung deliberately does not.**
|
|
143
|
+
On X11 it is grab, crosshair, click — and Escape cancels. The desktops
|
|
144
|
+
whose users expect a loupe have a portal or an `NSColorSampler` that draws
|
|
145
|
+
one; a hand-rolled loupe on the bottom rung would be a screenshot of the
|
|
146
|
+
screen re-rendered at 8×, permanently slightly wrong about scaling and
|
|
147
|
+
colour management.
|
|
148
|
+
- **Only the system rungs need no permission.** Both the portal and
|
|
149
|
+
`NSColorSampler` run the picker in another process and hand back a
|
|
150
|
+
colour, so nothing here asks for a screen-capture grant — on macOS that
|
|
151
|
+
is the difference between a dropper button and a TCC prompt for Screen
|
|
152
|
+
Recording.
|
|
153
|
+
- **On the X11 rung the keyboard also picks.** Return, KP Enter or space
|
|
154
|
+
samples the pixel under the pointer without a click — GTK's shape — and
|
|
155
|
+
the keyboard is grabbed for the duration so Escape works wherever the
|
|
156
|
+
focus happens to be.
|
|
157
|
+
- **What is sampled is what is on the screen.** The X11 rung reads the
|
|
158
|
+
rendered pixel, so a colour under a translucent overlay is the blend, not
|
|
159
|
+
the window's own value. What exactly the portal samples is the desktop's
|
|
160
|
+
choice, not this API's — GNOME's and KDE's pickers read the composited
|
|
161
|
+
screen too.
|
|
162
|
+
- **`signal` means less on the cocoa rung.** AppKit offers no way to dismiss
|
|
163
|
+
a sampler from code — the session ends when the user picks or presses
|
|
164
|
+
Escape — so an abort there ends the wait and nothing else: the loupe stays
|
|
165
|
+
up, and the colour it eventually reports is dropped. Until then that
|
|
166
|
+
pending sample holds the process's event loop open, the way pending I/O
|
|
167
|
+
does. The portal's request is Closed and the X11 grab is released, which
|
|
168
|
+
is what an abort has to guarantee where something _is_ held.
|
|
169
|
+
- **Every rung answers `'#rrggbb'`.** The portal and the sampler hand back
|
|
170
|
+
sRGB floats and X11 hands back server-layout words; every consumer wants a
|
|
171
|
+
CSS colour it can paint with, so the conversion happens once, here — the
|
|
172
|
+
same function for the two float rungs. If something later needs the
|
|
173
|
+
unrounded values, an option can widen the return — starting with channels
|
|
174
|
+
would have made the common case do arithmetic.
|
|
175
|
+
|
|
176
|
+
## The grab, and what it displaces
|
|
177
|
+
|
|
178
|
+
The X11 rung holds a **server pointer grab** while it waits. That is the
|
|
179
|
+
dangerous part of the feature — an application that leaks a grab leaves a
|
|
180
|
+
desktop that has stopped answering clicks — and it is why the grab's whole
|
|
181
|
+
lifecycle belongs to core rather than to apps:
|
|
182
|
+
|
|
183
|
+
- Every way out — click, Escape, abort, error — releases the grab before the
|
|
184
|
+
promise settles. The `signal` option releases it too; that is guaranteed
|
|
185
|
+
here, not delegated to the caller.
|
|
186
|
+
- A `<popup grab>` that was open when the pick started — an open `Select`,
|
|
187
|
+
a menu, the colour panel the eyedropper button itself sits in — had its
|
|
188
|
+
grab silently replaced by the pick's (same client, so X raises no error
|
|
189
|
+
and tells no one). When the pick ends, core hands that popup its grab
|
|
190
|
+
back, so dismiss-on-outside-click still works afterwards.
|
|
191
|
+
- A grab released out from under a pick — this client's own popup teardown
|
|
192
|
+
calls `UngrabPointer`, which releases whatever the client's active grab is
|
|
193
|
+
— is detected (the server announces it with a `LeaveNotify` of mode
|
|
194
|
+
`Ungrab` on the grab window) and taken back, rather than leaving a pick
|
|
195
|
+
that never resolves while the app answers clicks as if nothing were
|
|
196
|
+
happening.
|
|
197
|
+
|
|
198
|
+
## Running it anywhere
|
|
199
|
+
|
|
200
|
+
| where | rung |
|
|
201
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
202
|
+
| macOS, the cocoa backend, `@windowkit/appkit` >= 0.9 | cocoa — `NSColorSampler` ([macos.md](macos.md)) |
|
|
203
|
+
| macOS, the cocoa backend, an older bridge | none — `supported` is false, `pickScreenColor()` rejects typed |
|
|
204
|
+
| GNOME / KDE, or any portal with Screenshot ≥ 2 | portal |
|
|
205
|
+
| XFCE (portal present, no Screenshot interface) | X11 |
|
|
206
|
+
| ssh, `startx`, XQuartz, a container with a display | X11 |
|
|
207
|
+
| Node 20, where npm skips `dbus-native` | X11 |
|
|
208
|
+
| the Wayland backend, a portal with Screenshot ≥ 2 | portal |
|
|
209
|
+
| the Wayland backend on wlroots (sway, labwc, …) | wayland — the backend's own picker; there is no root window to read, so no X11 rung under it |
|
|
210
|
+
|
|
211
|
+
## Building a colour picker on it
|
|
212
|
+
|
|
213
|
+
The eyedropper is the one part of a colour picker that cannot be composed
|
|
214
|
+
out of public elements, which is why it lives in core and the picker panel
|
|
215
|
+
does not. A `<ColorPicker>` in `@react-x11/components` consumes it as a
|
|
216
|
+
function prop:
|
|
217
|
+
|
|
218
|
+
```jsx
|
|
219
|
+
<ColorPicker eyedropper={pickScreenColor} value={fill} onChange={setFill} />
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
and once that component exists, this function is its default and no app
|
|
223
|
+
changes.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# File dialogs
|
|
2
|
+
|
|
3
|
+
Open a file, save a file, pick a folder — on whatever the machine has.
|
|
4
|
+
|
|
5
|
+
```jsx
|
|
6
|
+
import { useFileDialog } from 'react-x11';
|
|
7
|
+
|
|
8
|
+
function Toolbar() {
|
|
9
|
+
const { openFile } = useFileDialog();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Button
|
|
13
|
+
label="Open…"
|
|
14
|
+
onPress={async () => {
|
|
15
|
+
const files = await openFile({
|
|
16
|
+
filters: [{ name: 'Markdown', extensions: ['md'] }],
|
|
17
|
+
});
|
|
18
|
+
if (!files) return; // cancelled
|
|
19
|
+
load(files[0]);
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Cancelling resolves to `null`. It is an ordinary outcome, not an exception, on
|
|
27
|
+
every backend — so the whole error path for a file dialog is one `if`.
|
|
28
|
+
|
|
29
|
+
## The ladder
|
|
30
|
+
|
|
31
|
+
There is no single answer to "show a file dialog" across the machines this
|
|
32
|
+
runs on, so this is a ladder and the rung is chosen for you:
|
|
33
|
+
|
|
34
|
+
| | | |
|
|
35
|
+
| --- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| 1 | **the native panel** | `NSOpenPanel` / `NSSavePanel` in this process, on the [cocoa backend](macos.md) (`@windowkit/appkit` ≥ 0.5). A **sheet** on the window that asked, with every filter the OS type database knows. On the [win32 backend](windows.md) Windows' own open/save dialog, owned by the window that asked, takes the same rung (`fileDialogBackend()` reports it as `'cocoa'` too). |
|
|
37
|
+
| 2 | **the portal** | `org.freedesktop.portal.FileChooser` over D-Bus. The desktop's own dialog, drawn by GTK or KDE in another process, with the user's bookmarks and recent files. |
|
|
38
|
+
| 3 | **`osascript`** | macOS on the X11 backend with no portal — which is every XQuartz install that has not gone out of its way. `choose file` _is_ `NSOpenPanel`, in a third process. |
|
|
39
|
+
| 4 | **the built-in dialog** | A browser react-x11 draws itself. ssh, a bare `startx`, a container: everywhere there is a display and nothing else. |
|
|
40
|
+
|
|
41
|
+
The top rung is never chosen by naming a backend: the dialog asks the window
|
|
42
|
+
it belongs to which connection it is on, and an app that can show native
|
|
43
|
+
panels says so. That is what keeps the ladder one ladder as backends join it.
|
|
44
|
+
|
|
45
|
+
`fileDialogBackend()` reports which one this machine lands on, without showing
|
|
46
|
+
anything — useful for a diagnostics panel, and for a menu that wants to say so.
|
|
47
|
+
|
|
48
|
+
**The ladder is the design, not a fallback chain bolted on.** react-x11's
|
|
49
|
+
flagship case is an app running on one machine and drawing to another, and a
|
|
50
|
+
file dialog that only worked on a full GNOME session would be useless to
|
|
51
|
+
exactly the person the renderer exists for.
|
|
52
|
+
|
|
53
|
+
## `useFileDialog()`
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const { openFile, saveFile, selectFolder } = useFileDialog(defaults?);
|
|
57
|
+
|
|
58
|
+
openFile(options?): Promise<string[] | null>; // absolute paths
|
|
59
|
+
saveFile(options?): Promise<string | null>; // one path, may not exist yet
|
|
60
|
+
selectFolder(options?): Promise<string[] | null>;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The hook is the surface to reach for, because it is the only one with a tree to
|
|
64
|
+
draw in — so it never runs out of rungs and never rejects for lack of a dialog.
|
|
65
|
+
|
|
66
|
+
| option | |
|
|
67
|
+
| ----------------------------- | ------------------------------------------------- |
|
|
68
|
+
| `title` | the dialog's title |
|
|
69
|
+
| `multiple` | several files at once; ignored when saving |
|
|
70
|
+
| `filters` | `{ name, extensions?, mimeTypes? }[]` |
|
|
71
|
+
| `defaultFolder` | where it opens |
|
|
72
|
+
| `defaultName` / `defaultPath` | what a save dialog starts on |
|
|
73
|
+
| `acceptLabel` | the confirm button's text — 'Import', 'Attach', … |
|
|
74
|
+
| `parentWindow` | override the owner window; inferred otherwise |
|
|
75
|
+
| `signal` | an `AbortSignal` that closes the dialog |
|
|
76
|
+
| `backend` | force a rung; the seam for a kiosk, and for tests |
|
|
77
|
+
|
|
78
|
+
Options given to the hook are defaults for every call; options given to a call
|
|
79
|
+
win.
|
|
80
|
+
|
|
81
|
+
### The dialog parents itself
|
|
82
|
+
|
|
83
|
+
The dialog is `transientFor` the window the component is in — `parent_window`
|
|
84
|
+
for the portal — which is what makes the window manager treat it as belonging
|
|
85
|
+
to your window: stacked above it, and not a second entry in the task switcher.
|
|
86
|
+
|
|
87
|
+
**You do not pass anything for that.** `useFileDialog()` resolves the owner
|
|
88
|
+
window through `useTopLevelWindow()` at the moment a dialog opens, by which
|
|
89
|
+
time the window is mounted and has an XID.
|
|
90
|
+
|
|
91
|
+
On the [native Wayland backend](wayland-backend.md) the window has no XID,
|
|
92
|
+
and the portal is handed an **xdg-foreign** export of it instead —
|
|
93
|
+
`wayland:<handle>`, which GNOME and KDE both parent to. Where the compositor
|
|
94
|
+
has no xdg-foreign the handle is empty and the dialog opens unparented.
|
|
95
|
+
|
|
96
|
+
Why inference rather than a lookup: a hook has no position in the host tree.
|
|
97
|
+
React context cannot come from a host element, the host context
|
|
98
|
+
`getChildHostContext` builds reaches `createInstance` and not components, and a
|
|
99
|
+
`<window>` has no XID until the commit phase. So it answers from what the
|
|
100
|
+
renderer does know — the top-level windows this connection is rendering:
|
|
101
|
+
|
|
102
|
+
- **one top-level window is exact.** The component is in it, because there is
|
|
103
|
+
nowhere else to be. That is nearly every app.
|
|
104
|
+
- several, one focused: that one. You clicked in it a moment ago, which is why
|
|
105
|
+
a dialog is opening.
|
|
106
|
+
- several with nothing to separate them: the most recently opened, plus a
|
|
107
|
+
development warning naming `parentWindow` as the way to be exact.
|
|
108
|
+
- **none at all: the root-level `<popup>` that took the keyboard.** A menu-bar
|
|
109
|
+
app is a tray item and the popover a click on it opens, and nothing else
|
|
110
|
+
([elements.md](elements.md#a-popover-under-a-tray-item)) — so the popup is
|
|
111
|
+
the top of that tree, and answering nothing left every hook that reads this
|
|
112
|
+
one with no owner to belong to.
|
|
113
|
+
|
|
114
|
+
So `parentWindow` survives as the **override for multi-window apps**, and takes
|
|
115
|
+
a `<window>` ref, an XID, or anything `windowIdOf()` accepts:
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
const win = useRef(null);
|
|
119
|
+
const { openFile } = useFileDialog({ parentWindow: win });
|
|
120
|
+
return <window ref={win}>…</window>;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`useTopLevelWindow()` is exported for anything else that needs the owner
|
|
124
|
+
window. It returns a **ref-like object** — the window is not realized on the
|
|
125
|
+
first render, so a value read then would be `null` on the render that matters —
|
|
126
|
+
which drops into `parentWindow`, `transientFor`, or `windowIdOf()`.
|
|
127
|
+
|
|
128
|
+
**The portal never embeds the dialog.** It is a top-level window in another
|
|
129
|
+
process, and logical parenting is all there is. There is no version of this
|
|
130
|
+
where the file list appears inside your window.
|
|
131
|
+
|
|
132
|
+
## The imperative functions
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { openFile, saveFile, selectFolder } from 'react-x11';
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Same options, same results — but they can only reach the native panel, the
|
|
139
|
+
portal and `osascript`, because a function has nowhere to draw. Where there is
|
|
140
|
+
none of those they reject with `NoFileDialogError`, which is a **typed**
|
|
141
|
+
rejection: the signal to show your own UI, not a crash.
|
|
142
|
+
|
|
143
|
+
Reach for these from host-side code and event-loop glue that has no component
|
|
144
|
+
to hang off. In a component, use the hook.
|
|
145
|
+
|
|
146
|
+
## What differs between the rungs
|
|
147
|
+
|
|
148
|
+
Most of the API is the same everywhere. These are the places it genuinely is
|
|
149
|
+
not, stated here rather than discovered:
|
|
150
|
+
|
|
151
|
+
- **MIME-type filters do not reach `osascript`.** `extensions` translate
|
|
152
|
+
exactly to every rung; `mimeTypes` reach the portal and the native panel
|
|
153
|
+
(the cocoa backend asks the OS's own type database, so `image/png` is
|
|
154
|
+
`public.png` there) and are dropped by `osascript`, because AppleScript's
|
|
155
|
+
`of type` wants extensions or UTIs and guessing a UTI wrong hides the user's
|
|
156
|
+
file with no way to get at it. Give `extensions` where you can.
|
|
157
|
+
- **`osascript` ignores `parentWindow`; the native panel is a sheet on it.**
|
|
158
|
+
XQuartz windows are `NSWindow`s owned by X11.app, macOS has no cross-process
|
|
159
|
+
transient-for, and `osascript` is a third process besides: its panel
|
|
160
|
+
appears over the app but is not attached to it, and application modality
|
|
161
|
+
works only because your code is awaiting the promise. On the cocoa backend
|
|
162
|
+
the panel is a **sheet** on that window, and the app keeps running while it
|
|
163
|
+
is up. With no window at all — a bare `openFile()` with nothing in
|
|
164
|
+
`parentWindow` and no tree to infer one from — the native panel is
|
|
165
|
+
app-modal and **blocks the process**, timers included, until it is
|
|
166
|
+
dismissed. `useFileDialog()` always names the window, so it never lands
|
|
167
|
+
there.
|
|
168
|
+
- **The built-in dialog has never seen the user's bookmarks.** It offers the
|
|
169
|
+
filesystem, a filter, hidden files, and a path you can type into. It is
|
|
170
|
+
deliberately not a re-creation of GTK's chooser.
|
|
171
|
+
- **`multiple` in the built-in dialog is a tick column**, not ctrl-click.
|
|
172
|
+
|
|
173
|
+
In the built-in dialog a double click opens a directory or picks a file, Enter
|
|
174
|
+
does the same to the selected row, `Up` goes to the parent, and Escape cancels
|
|
175
|
+
— and a selected directory is drawn in the selection's own text colour rather
|
|
176
|
+
than the accent it wears at rest, since the selected row is filled with that
|
|
177
|
+
accent in both schemes.
|
|
178
|
+
|
|
179
|
+
## Running it where there is no bus
|
|
180
|
+
|
|
181
|
+
The interesting configurations, and what each one does:
|
|
182
|
+
|
|
183
|
+
| where | rung |
|
|
184
|
+
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| macOS, the **cocoa backend** | the native panel |
|
|
186
|
+
| GNOME / KDE / any desktop with xdg-desktop-portal | portal |
|
|
187
|
+
| macOS + XQuartz, app running **on the Mac** | `osascript` |
|
|
188
|
+
| macOS + XQuartz, app running on a **remote Linux box** | built-in — `osascript` would run on the wrong machine, and the paths it returned would not exist where the app is |
|
|
189
|
+
| ssh, `startx`, a container, CI | built-in |
|
|
190
|
+
| Node 20, where npm skips `dbus-native` | built-in on Linux, `osascript` on a Mac |
|
|
191
|
+
|
|
192
|
+
The `osascript` rung is reached by _not_ finding a portal, so it also covers a
|
|
193
|
+
Mac that has a session bus for other reasons. If you do run a portal shim on a
|
|
194
|
+
Mac (see [issue #111](https://github.com/sidorares/react-x11/issues/111)), it
|
|
195
|
+
wins on the X11 backend, which is what you asked for by running it; on the
|
|
196
|
+
cocoa backend the native panel is above it either way.
|
|
197
|
+
|
|
198
|
+
## The portal machinery, if you need it directly
|
|
199
|
+
|
|
200
|
+
`portalRequest()` is exported for building other portals on. Every portal
|
|
201
|
+
method that shows UI returns an object path immediately and answers later with
|
|
202
|
+
a `Request.Response` signal — and the path is predictable **by design**, so the
|
|
203
|
+
client subscribes _before_ it calls and cannot lose the answer:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
const { response, results } = await portalRequest(busRef, {
|
|
207
|
+
iface: 'org.freedesktop.portal.FileChooser',
|
|
208
|
+
member: 'OpenFile',
|
|
209
|
+
parentWindow: 'x11:1a00007',
|
|
210
|
+
title: 'Open',
|
|
211
|
+
options: { multiple: true },
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
It owns the three things every portal caller otherwise re-solves: subscribing
|
|
216
|
+
first, `Request.Close()` on abort (which emits **no** `Response`, so the
|
|
217
|
+
promise has to be settled locally), and no timeout on the answer — a dialog can
|
|
218
|
+
legitimately be open for an hour, and only the initial call is deadlined.
|
|
219
|
+
|
|
220
|
+
The default argument shape is FileChooser's, `(s parent_window, s title, a{sv}
|
|
221
|
+
options)` — and that shape is FileChooser's alone. Every other Request-shaped
|
|
222
|
+
portal has its own leading arguments, so a caller states them, as `signature`
|
|
223
|
+
and `args` together; the [eyedropper](eyedropper.md)'s `PickColor`, which has
|
|
224
|
+
no title, is the worked example:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
await portalRequest(busRef, {
|
|
228
|
+
iface: 'org.freedesktop.portal.Screenshot',
|
|
229
|
+
member: 'PickColor',
|
|
230
|
+
signature: 'sa{sv}',
|
|
231
|
+
args: [parentWindow],
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The signature must end in `a{sv}` — the options dict is where `handle_token`
|
|
236
|
+
rides, so a method without one does not answer through a `Request` and wants a
|
|
237
|
+
plain `bus.invoke` instead.
|
|
238
|
+
|
|
239
|
+
`hasService(name)` answers whether a service is reachable — owned now **or
|
|
240
|
+
activatable on demand**. `NameHasOwner` alone is the wrong question:
|
|
241
|
+
`org.freedesktop.portal.Desktop` is D-Bus-activatable, so on a healthy desktop
|
|
242
|
+
where no app has touched a portal yet it has no owner, and a feature gated on
|
|
243
|
+
that takes the fallback path forever.
|
|
244
|
+
|
|
245
|
+
`portalVersion(iface)` answers the question `hasService()` cannot: whether the
|
|
246
|
+
portal's backends provide one _interface_, and at what version — `0` when it
|
|
247
|
+
is not there at all. Capability lives in the interface's own `version`
|
|
248
|
+
property, and gating on the service alone breaks exactly on the desktops that
|
|
249
|
+
provide some portals and not others (XFCE has FileChooser and no Screenshot).
|
|
250
|
+
|
|
251
|
+
See [dbus.md](dbus.md) for the connection these sit on.
|
|
252
|
+
|
|
253
|
+
## Seeing it
|
|
254
|
+
|
|
255
|
+
- `npm run examples:menu` — a File menu whose Open/Save entries are real, with
|
|
256
|
+
the rung this machine landed on shown in the window.
|
|
257
|
+
Force the bottom rung anywhere with `backend: 'builtin'`, which is also how the
|
|
258
|
+
tests exercise it.
|