react-x11 2.52.0 → 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.
Files changed (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -24,47 +24,34 @@ function StrokePicker({ value, onChange }) {
24
24
 
25
25
  Cancelling — Escape, the desktop dialog's own cancel, a dismissed macOS
26
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.
27
+ on every rung, so the whole error path is one `if`, as with the
28
+ [file dialogs](filedialog.md).
29
29
 
30
30
  ## The ladder
31
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. |
32
+ | | | |
33
+ | --- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | 1 | **the system sampler** | `NSColorSampler` on the [cocoa backend](macos.md) (`@windowkit/appkit` >= 0.9). macOS draws the loupe out of process, so the app needs no Screen Recording grant and the user gets the usual Mac magnifier. |
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 overlay and the click is taken on that. |
37
+ | 4 | **X11** | Grab the pointer with a crosshair, wait for the click, read the pixel under it. Works under a bare WM, over ssh, on XQuartz — everywhere there is a display and nothing else. |
38
38
 
39
39
  `screenColorBackend()` reports which rung this machine lands on without
40
40
  grabbing anything; `useEyedropper().supported` is the same answer as render
41
41
  state.
42
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.
43
+ There is no rung that react-x11 draws itself — the screen is exactly what an
44
+ app cannot draw — so the ladder can run out. Where it does, `supported` is
45
+ `false` and a pick rejects with a typed error, never a crash. A cocoa app on a
46
+ bridge **older than 0.9** is that place ([macos.md](macos.md)): hide the
47
+ button there.
58
48
 
59
49
  ### The version gate
60
50
 
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):
51
+ `PickColor` arrived in **version 2** of the Screenshot interface, and a portal
52
+ on the bus may not have it (XFCE's ships no Screenshot interface at all). The
53
+ probe reads the interface's own `version` with `portalVersion()`, which is
54
+ exported:
68
55
 
69
56
  ```js
70
57
  import { portalVersion } from 'react-x11';
@@ -73,8 +60,7 @@ const v = await portalVersion('org.freedesktop.portal.Screenshot');
73
60
  // 0 — not there at all; 1 — there, but before PickColor; 2+ — usable
74
61
  ```
75
62
 
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.
63
+ Below 2, the ladder falls through to the X11 rung.
78
64
 
79
65
  ## `useEyedropper()`
80
66
 
@@ -94,21 +80,17 @@ picking: boolean; // a pick is in flight — the button's pressed state
94
80
 
95
81
  Options given to the hook are defaults for every pick; options given to a
96
82
  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
83
+ resolved when the pick starts — the
84
+ [`useFileDialog()` inference](filedialog.md#the-dialog-parents-itself), with
98
85
  `parentWindow` as the override for a tree with several top-level windows.
99
86
 
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.
87
+ - **`picking` is the pressed state.** While it is true, a second `pick()`
88
+ returns the promise already in flight rather than starting a second grab,
89
+ so a double-clicked button cannot make the user click twice.
90
+ - **`supported` is almost always true.** An X11 tree's connection _is_ the
91
+ fallback rung, and a cocoa tree has the system sampler. It goes false when a
92
+ forced `backend: 'portal'` finds no portal, and on a cocoa app whose bridge
93
+ predates the sampler.
112
94
 
113
95
  ## The bare function
114
96
 
@@ -118,82 +100,55 @@ import { pickScreenColor, screenColorBackend } from 'react-x11';
118
100
  const hex = await pickScreenColor({ app });
119
101
  ```
120
102
 
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.
103
+ Same options, same results. The one addition is `app`, because a bare function
104
+ has no tree to take a connection from: pass the app `createRoot()` returned
105
+ (or `useApp()` in a component, or a `parentWindow` that points at a mounted
106
+ node). With no app named, the connections the renderer is currently drawing
107
+ through are asked. Where no rung answers it rejects with `NoScreenColorError`
108
+ — a **typed** rejection, so it reads as "hide the button", not as a crash.
130
109
 
131
110
  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.
111
+ `pickScreenColor()` on the same app is refused loudly (two grabs from one
112
+ client would steal each other's click). The hook's shared in-flight promise is
113
+ the friendly version of the same rule. On the system sampler a second pick
114
+ joins the loupe that is up, and both callers get the same colour.
139
115
 
140
116
  ## What differs between the rungs
141
117
 
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.
118
+ - **The system rungs draw a magnifier; the X11 rung does not.** On X11 it is
119
+ grab, crosshair, click — and Escape cancels.
120
+ - **Only the system rungs need no permission.** The portal and
121
+ `NSColorSampler` run the picker in another process, so nothing asks for a
122
+ screen-capture grant (on macOS, no TCC prompt for Screen Recording).
153
123
  - **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.
124
+ samples the pixel under the pointer without a click, and the keyboard is
125
+ grabbed for the duration so Escape works wherever the focus is.
157
126
  - **What is sampled is what is on the screen.** The X11 rung reads the
158
127
  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.
128
+ the window's own value. The portal's pickers (GNOME's, KDE's) read the
129
+ composited screen too.
130
+ - **`signal` means less on the cocoa rung.** AppKit cannot dismiss a sampler
131
+ from code, so an abort there ends the wait and nothing else: the loupe stays
132
+ up until the user picks or presses Escape, its colour is dropped, and until
133
+ then the pending sample holds the process's event loop open. On the portal
134
+ and X11 rungs an abort closes the request or releases the grab.
135
+ - **Every rung answers `'#rrggbb'`**, converted from the portal's and
136
+ sampler's sRGB floats or X11's server-layout pixel.
175
137
 
176
138
  ## The grab, and what it displaces
177
139
 
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:
140
+ The X11 rung holds a **server pointer grab** while it waits, and core owns its
141
+ whole lifecycle:
182
142
 
183
143
  - 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.
144
+ promise settles.
145
+ - A `<popup grab>` that was open when the pick started (an open `Select`, a
146
+ menu, the colour panel the button sits in) gets its grab back when the pick
147
+ ends, so dismiss-on-outside-click still works afterwards.
148
+ - If this client's own popup teardown releases the grab mid-pick, the pick
149
+ takes it back rather than hanging.
150
+
151
+ Design notes: [design/eyedropper.md](../design/eyedropper.md).
197
152
 
198
153
  ## Running it anywhere
199
154
 
@@ -207,17 +162,3 @@ lifecycle belongs to core rather than to apps:
207
162
  | Node 20, where npm skips `dbus-native` | X11 |
208
163
  | the Wayland backend, a portal with Screenshot ≥ 2 | portal |
209
164
  | 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.
@@ -28,27 +28,19 @@ every backend — so the whole error path for a file dialog is one `if`.
28
28
 
29
29
  ## The ladder
30
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:
31
+ The rung is chosen for you, top to bottom:
33
32
 
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. |
33
+ | | | |
34
+ | --- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | 1 | **the native panel** | `NSOpenPanel` / `NSSavePanel` 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 `'cocoa'` for both). |
36
+ | 2 | **the portal** | `org.freedesktop.portal.FileChooser` over D-Bus: the desktop's own GTK or KDE dialog, with the user's bookmarks and recent files. |
37
+ | 3 | **`osascript`** | macOS on the X11 backend with no portal (every ordinary XQuartz install). `choose file` _is_ `NSOpenPanel`, in a third process. |
38
+ | 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
39
 
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.
40
+ The top rung is chosen by the window the dialog belongs to, never by naming a
41
+ backend. `fileDialogBackend()` reports which rung this machine lands on,
42
+ without showing anything — for a diagnostics panel, or a menu that wants to
43
+ say so.
52
44
 
53
45
  ## `useFileDialog()`
54
46
 
@@ -60,8 +52,8 @@ saveFile(options?): Promise<string | null>; // one path, may not exist yet
60
52
  selectFolder(options?): Promise<string[] | null>;
61
53
  ```
62
54
 
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.
55
+ The hook is the surface to reach for: it has a tree to draw the built-in dialog
56
+ in, so it never runs out of rungs and never rejects for lack of a dialog.
65
57
 
66
58
  | option | |
67
59
  | ----------------------------- | ------------------------------------------------- |
@@ -80,39 +72,29 @@ win.
80
72
 
81
73
  ### The dialog parents itself
82
74
 
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:
75
+ The dialog is `transientFor` the window the component is in (`parent_window`
76
+ for the portal), so the window manager stacks it above your window and keeps
77
+ it out of the task switcher. **You do not pass anything for that**:
78
+ `useFileDialog()` resolves the owner window through `useTopLevelWindow()` when
79
+ the dialog opens.
80
+
81
+ On the [native Wayland backend](wayland-backend.md) the portal is handed an
82
+ **xdg-foreign** export of the window (`wayland:<handle>`), which GNOME and KDE
83
+ both parent to. Where the compositor has no xdg-foreign the dialog opens
84
+ unparented.
85
+
86
+ The owner is inferred from the top-level windows this connection is rendering:
87
+
88
+ - **one top-level window** — that one; nearly every app.
89
+ - several, one focused — the focused one.
90
+ - several with nothing to separate them — the most recently opened, plus a
91
+ development warning naming `parentWindow`.
92
+ - **none at all** — the root-level `<popup>` that took the keyboard (a
93
+ menu-bar app's popover,
94
+ [elements.md](elements.md#a-popover-under-a-tray-item)).
95
+
96
+ `parentWindow` is the **override for multi-window apps**, and takes a
97
+ `<window>` ref, an XID, or anything `windowIdOf()` accepts:
116
98
 
117
99
  ```jsx
118
100
  const win = useRef(null);
@@ -126,8 +108,9 @@ first render, so a value read then would be `null` on the render that matters
126
108
  which drops into `parentWindow`, `transientFor`, or `windowIdOf()`.
127
109
 
128
110
  **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.
111
+ process; the file list never appears inside your window.
112
+
113
+ Design notes: [design/filedialog.md](../design/filedialog.md).
131
114
 
132
115
  ## The imperative functions
133
116
 
@@ -145,41 +128,30 @@ to hang off. In a component, use the hook.
145
128
 
146
129
  ## What differs between the rungs
147
130
 
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
131
  - **MIME-type filters do not reach `osascript`.** `extensions` translate
152
132
  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.
133
+ (the cocoa backend maps `image/png` to `public.png` through the OS type
134
+ database) and are dropped by `osascript`, where a wrong UTI guess would hide
135
+ the user's file. Give `extensions` where you can.
157
136
  - **`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
137
+ The `osascript` panel appears over the app but is not attached to it;
138
+ application modality holds only because your code is awaiting the promise.
139
+ On the cocoa backend the panel is a **sheet**, and the app keeps running
140
+ while it is up. With no window at all — a bare `openFile()` with nothing in
164
141
  `parentWindow` and no tree to infer one from — the native panel is
165
142
  app-modal and **blocks the process**, timers included, until it is
166
143
  dismissed. `useFileDialog()` always names the window, so it never lands
167
144
  there.
168
145
  - **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.
146
+ filesystem, a filter, hidden files, and a path you can type into — not a
147
+ re-creation of GTK's chooser.
171
148
  - **`multiple` in the built-in dialog is a tick column**, not ctrl-click.
172
149
 
173
150
  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.
151
+ does the same to the selected row, `Up` goes to the parent, and Escape cancels.
178
152
 
179
153
  ## Running it where there is no bus
180
154
 
181
- The interesting configurations, and what each one does:
182
-
183
155
  | where | rung |
184
156
  | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
185
157
  | macOS, the **cocoa backend** | the native panel |
@@ -190,17 +162,13 @@ The interesting configurations, and what each one does:
190
162
  | Node 20, where npm skips `dbus-native` | built-in on Linux, `osascript` on a Mac |
191
163
 
192
164
  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.
165
+ Mac that has a session bus for other reasons. A portal shim on a Mac wins on
166
+ the X11 backend; on the cocoa backend the native panel is above it either way.
197
167
 
198
168
  ## The portal machinery, if you need it directly
199
169
 
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:
170
+ `portalRequest()` is exported for building other portals on. It calls a portal
171
+ method that shows UI and resolves with its `Request.Response`:
204
172
 
205
173
  ```js
206
174
  const { response, results } = await portalRequest(busRef, {
@@ -212,16 +180,15 @@ const { response, results } = await portalRequest(busRef, {
212
180
  });
213
181
  ```
214
182
 
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.
183
+ It subscribes to the response before calling, so the answer cannot be lost;
184
+ an abort sends `Request.Close()` and settles the promise locally (the portal
185
+ emits no `Response` for it); and only the initial call has a deadline — the
186
+ answer has none, because a dialog can legitimately be open for an hour.
219
187
 
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:
188
+ The default argument shape, `(s parent_window, s title, a{sv} options)`, is
189
+ FileChooser's alone. Any other Request-shaped portal states its leading
190
+ arguments as `signature` and `args` together; the
191
+ [eyedropper](eyedropper.md)'s `PickColor`, which has no title:
225
192
 
226
193
  ```js
227
194
  await portalRequest(busRef, {
@@ -232,21 +199,18 @@ await portalRequest(busRef, {
232
199
  });
233
200
  ```
234
201
 
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.
202
+ The signature must end in `a{sv}` (the options dict carries `handle_token`); a
203
+ method without one does not answer through a `Request` and wants a plain
204
+ `bus.invoke` instead.
238
205
 
239
206
  `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).
207
+ activatable on demand**. (`NameHasOwner` alone is the wrong question: the
208
+ portal is D-Bus-activatable and often has no owner until first use.)
209
+
210
+ `portalVersion(iface)` answers whether the portal's backends provide one
211
+ _interface_, and at what version — `0` when it is not there at all. Gate on
212
+ this rather than on the service: desktops provide some portals and not others
213
+ (XFCE has FileChooser and no Screenshot).
250
214
 
251
215
  See [dbus.md](dbus.md) for the connection these sit on.
252
216