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.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -0,0 +1,663 @@
1
+ # Drag and drop
2
+
3
+ react-x11 speaks [XDND](https://freedesktop.org/wiki/Specifications/XDND/),
4
+ the X11 drag-and-drop protocol, in both directions: a file dragged out of
5
+ Nautilus lands on a `<box>` as an ordinary event, and a row dragged out of
6
+ your app can be dropped into a file manager, an editor or a browser. The
7
+ same API also covers drags that never leave the app — a reorderable list, a
8
+ kanban board — without any X11 traffic at all.
9
+
10
+ Two prop families, and nothing else to install:
11
+
12
+ | you want | props |
13
+ | ------------- | ---------------------------------------------------------------------------- |
14
+ | accept a drop | `dropAccept`, `onDragEnter`, `onDragOver`, `onDragLeave`, `onDrop` |
15
+ | start a drag | `draggable`, `dragData`, `dragActions`, `onDragStart`, `onDrag`, `onDragEnd` |
16
+
17
+ Both work on any drawn element and on `<window>` / `<popup>`. Their
18
+ _presence_ is what registers the node — there is no provider to mount and
19
+ no context to thread.
20
+
21
+ > The design rationale — why the API is shaped this way, why the XDND
22
+ > advertisement is unconditional, what was rejected — is a separate
23
+ > document: [architecture/drag-and-drop.md](architecture/drag-and-drop.md).
24
+ > This page is the reference.
25
+
26
+ ## Accepting drops
27
+
28
+ ```jsx
29
+ <box
30
+ dropAccept={['files']}
31
+ onDrop={(e) => setPaths(e.files.map((f) => f.path))}
32
+ style={{
33
+ borderColor: '$border',
34
+ ':drag-over': { borderColor: '$accent' },
35
+ }}
36
+ />
37
+ ```
38
+
39
+ That is a complete dropzone: it accepts file drags from any XDND
40
+ application, highlights while one is over it, and receives decoded local
41
+ paths. No state, no effect, no cleanup.
42
+
43
+ ### `dropAccept`
44
+
45
+ What the node will take. Four shapes:
46
+
47
+ | value | means |
48
+ | ------------------------------------ | --------------------------------------------------------- |
49
+ | `'image/png'` | exactly that type (MIME names compare case-insensitively) |
50
+ | `['files']` | a semantic group — see the table below |
51
+ | `['image/png', 'text', 'x-my/type']` | any of these |
52
+ | `(types) => boolean` | your own predicate, given the offered type names |
53
+ | _absent_ | anything — a bare `onDrop` is a valid dropzone |
54
+
55
+ The three groups exist because the same payload arrives under different
56
+ names depending on who is dragging, and exact-match filtering silently
57
+ misses most of the desktop. The clipboard uses the same table, on purpose —
58
+ what a file manager offers on a drop is what it offers on a copy, so
59
+ [`clipboard.read('files')`](clipboard.md#reading) resolves it the same way:
60
+
61
+ | group | matches |
62
+ | --------- | ---------------------------------------------------------------------------------------------------------- |
63
+ | `'files'` | `text/uri-list`, `application/x-kde4-urilist`, `x-special/gnome-copied-files` |
64
+ | `'uris'` | `text/uri-list`, `text/x-moz-url`, `_NETSCAPE_URL` |
65
+ | `'text'` | `text/plain;charset=utf-8`, `UTF8_STRING`, `text/plain`, `STRING`, `TEXT`, `COMPOUND_TEXT` (in that order) |
66
+
67
+ `dropAccept={['text']}` therefore catches GTK text (`UTF8_STRING`), a
68
+ terminal selection (`STRING`) and a modern editor's
69
+ `text/plain;charset=utf-8` alike, where `dropAccept={['text/plain']}`
70
+ catches only the last of those. Groups do not overlap: a Firefox link is
71
+ `text/x-moz-url`, which is in `uris`, so accept `['uris', 'text']` if you
72
+ want both. A group only ever expands to its own list.
73
+
74
+ `dropAccept` is **data, not a callback**, and that is load-bearing rather
75
+ than stylistic: the protocol's accept/reject answer is computed straight
76
+ from it, inside the X message handler, without entering React. A window
77
+ that is slow to answer stalls the drag cursor in _the other application_,
78
+ which is a user-visible freeze in a program you do not own.
79
+
80
+ ### The drag events
81
+
82
+ `onDragEnter` and `onDragLeave` do not propagate — they are synthesized by
83
+ path diffing, exactly like `onMouseEnter`/`onMouseLeave`. `onDragOver` and
84
+ `onDrop` dispatch capture → target → bubble like any other event
85
+ (`onDragOverCapture`, `onDropCapture`), `onDragOver` once per pointer
86
+ position.
87
+
88
+ The order across one whole drag, source events and target events
89
+ interleaved:
90
+
91
+ ```
92
+ onDragStart the pointer passed the threshold
93
+ ┌ onDragLeave (target) nodes the drag moved off, deepest first
94
+ │ onDragEnter (target) nodes it moved onto, outermost first
95
+ │ onDragOver (target) the node under the pointer, capture → bubble
96
+ └ onDrag (source) last, once the target has been told
97
+ onDrop the release, capture → bubble
98
+ onDragEnd always last, on the source
99
+ ```
100
+
101
+ The four middle handlers repeat per pointer position, in that order — the
102
+ target learns where the pointer is before the source hears about the move,
103
+ which is why `onDrag`'s `accepted` is already up to date when it runs.
104
+
105
+ `onDragEnd` runs even when nothing accepted the drop — with `action: null`
106
+ and `dropped: false` — so releasing state there is safe.
107
+
108
+ Which node is _the_ target, when they nest: the accept decision picks the
109
+ **deepest** node whose `dropAccept` matches, skipping any node marked
110
+ `disabled`, so a `<window>`-level `dropAccept` is a fallback that loses to
111
+ any matching descendant. Two wrinkles worth knowing:
112
+
113
+ - `onDrop` still dispatches capture → bubble along the whole ancestor path
114
+ of the node under the pointer, so an ancestor with an `onDrop` sees the
115
+ drop even if its own `dropAccept` did not match. If that is not what you
116
+ want, `e.stopPropagation()` in the inner handler.
117
+ - `onDragEnter`, `onDragLeave` and `':drag-over'` are **not** filtered at
118
+ all — not by `dropAccept`, not by `disabled`. They follow the pointer's
119
+ ancestor path, exactly like hover.
120
+
121
+ Every drag event carries:
122
+
123
+ | field | |
124
+ | -------------------------- | ------------------------------------------------------------------------------ |
125
+ | `types` | `string[]` — the offered payload type names |
126
+ | `has(type)` | group-aware membership test: `e.has('files')` |
127
+ | `action` | `'copy' \| 'move' \| 'link' \| 'ask' \| 'private'` — what the source asked for |
128
+ | `actions` | the choices an `'ask'` source will accept; empty otherwise |
129
+ | `actionDescriptions` | its own words for them, positionally matched, `null` where it gave none |
130
+ | `source` | `'internal'` (this app) or `'external'` (another application) |
131
+ | `screenX` / `screenY` | pointer position in root coordinates |
132
+ | `x`/`y`, `localX`/`localY` | window and target-relative coordinates, as on any event |
133
+
134
+ plus three ways to answer. `e.freeze()` is `onDragOver` only; `e.accept()`
135
+ and `e.reject()` also work in `onDrop`, where they settle what the drop did
136
+ rather than whether it would be taken (see [The drop](#the-drop)):
137
+
138
+ - `e.accept(action?)` — take the drop, optionally forcing the action
139
+ (`e.accept('move')`);
140
+ - `e.reject()` — refuse it, overriding a `dropAccept` that matched;
141
+ - `e.freeze()` — tell the source to stop sending positions while the
142
+ pointer stays inside this node's rectangle. Cheap on a remote display,
143
+ but it makes the node deaf: **do not freeze a zone that draws an
144
+ insertion caret or auto-scrolls near its edges.** It applies to drags
145
+ from other applications only; during an in-app drag it does nothing,
146
+ since there is no source to tell.
147
+
148
+ ### The drop
149
+
150
+ `onDrop` may be `async`. For a drop from **another application** the
151
+ protocol reply is held until the returned promise settles, so `await`ing a
152
+ write is correct; a watchdog sends the reply anyway after 10 s, because a
153
+ forgotten `await` in your app must not hang the drag gesture in someone
154
+ else's.
155
+
156
+ For an **in-app** drop there is no protocol reply to hold, so nothing waits:
157
+ `onDragEnd` fires immediately and an async `onDrop` finishes on its own
158
+ afterwards. Do not rely on the drag being "over" only once your handler has
159
+ resolved — if the source needs to know, have the handler tell it.
160
+
161
+ The event adds three ways to reach the payload:
162
+
163
+ ```jsx
164
+ onDrop={async (e) => {
165
+ e.files; // parsed text/uri-list
166
+ e.text; // the best offered text flavour
167
+ const png = await e.getData('image/png'); // anything else
168
+ }}
169
+ ```
170
+
171
+ - **`e.files`** — `[{ uri, path? }]`, parsed per RFC 2483: CRLF-separated,
172
+ `#` comment lines dropped, percent-decoding done. `path` is present only
173
+ for genuinely local `file:` URIs — a remote `file://otherhost/…` has a
174
+ `uri` and no `path`, rather than a path that does not exist here.
175
+ - **`e.text`** — the best text flavour the source offered, decoded. For a
176
+ file drag it is the raw `text/uri-list` payload instead, which is what
177
+ `e.files` was parsed from; and it is `undefined` when nothing text-ish
178
+ was offered or the conversion failed.
179
+ - **`await e.getData(type)`** — one conversion of any offered type. Text-ish
180
+ targets decode to a string, everything else stays raw bytes. Group names
181
+ work: `getData('files')` resolves to whichever concrete type was offered.
182
+ - **`parseUriList(text)`** — exported from the package, and the same parser
183
+ `e.files` uses. For when you fetch the list yourself:
184
+ `parseUriList(await e.getData('text/uri-list'))`.
185
+ - **`e.items`** — _internal drags only_: the `dragData` values themselves,
186
+ by reference, never serialised. `undefined` for drops from other
187
+ applications, which is also how you tell the two apart without checking
188
+ `e.source`.
189
+
190
+ There is deliberately **no `e.dataTransfer`**. X selection transfer is
191
+ asynchronous and the DOM's `getData()` is synchronous; a lookalike
192
+ returning a promise would stringify to `"[object Promise]"` in template
193
+ literals with no error anywhere. The different name is the warning.
194
+
195
+ `onDrop` can also settle what the drop _did_, which is what the source is
196
+ told afterwards:
197
+
198
+ - **`e.accept('move')`** — report a different action from the one that was
199
+ negotiated while hovering. This is how a source that asks gets its
200
+ answer.
201
+ - **`e.reject()`** — having looked at the payload, refuse it after all.
202
+ The source is told the drop was not taken, so a "move" leaves the
203
+ original where it was.
204
+
205
+ Both may be called late from an `async` handler: for a drag from another
206
+ application the reply is not sent until the handler settles, so a menu can
207
+ decide it. In-app drags do not wait, so only a synchronous answer reaches
208
+ `onDragEnd`.
209
+
210
+ ### When the handler fails
211
+
212
+ A handler that throws — or returns a promise that rejects — did not store
213
+ what it was given, so **the drop is refused**, exactly as if it had called
214
+ `e.reject()`, and the failure is reported the way every handler throw is
215
+ ([events.md](events.md#when-a-handler-throws)): the handler name, the
216
+ element, and the component that rendered it.
217
+
218
+ Refusing is the part worth knowing about. A `move` source deletes its own
219
+ copy on the strength of being told the drop was taken, so a failed import
220
+ that reported success is how a file goes missing. A later handler on the
221
+ path can still `accept()`; the refusal is the failed one's answer, not the
222
+ whole dispatch's.
223
+
224
+ An `async` failure is reported when the promise rejects. For a drop from
225
+ another application that is before the protocol reply goes out, so the
226
+ source hears about it. In-app it is after `onDragEnd` has already fired,
227
+ for the same reason nothing else in that path waits — which is why it is
228
+ reported rather than only returned.
229
+
230
+ ### Sources that ask
231
+
232
+ A file manager dragging between two filesystems often does not know
233
+ whether you meant copy or move, so it asks: `e.action` is `'ask'`, and the
234
+ choices it will accept come with the event.
235
+
236
+ ```jsx
237
+ onDrop={async (e) => {
238
+ if (e.action !== 'ask') return void save(e.files, e.action);
239
+ // e.actions -> ['copy', 'move', 'link']
240
+ // e.actionDescriptions -> ['Copy here', 'Move here', null]
241
+ const chosen = await myMenu(e.actions, e.actionDescriptions);
242
+ if (!chosen) return void e.reject();
243
+ save(e.files, chosen);
244
+ e.accept(chosen);
245
+ }}
246
+ ```
247
+
248
+ `actionDescriptions` is positional and the source's own wording — a `null`
249
+ means it offered that action without describing it, so use your own label.
250
+ Both arrays are empty for every other kind of drag, so they can be read
251
+ without guarding, and nothing is read off the source's window unless a
252
+ position actually asks.
253
+
254
+ The menu is yours to draw. A built-in one would have to hold the protocol
255
+ reply open for as long as it stayed up, and that reply is the only thing
256
+ keeping the other application's drag cursor from freezing — so the choice
257
+ of how long to make someone wait belongs to the app, not the toolkit.
258
+ `ContextMenu` is the obvious thing to build it from.
259
+
260
+ ### `:drag-over`
261
+
262
+ While a drag is in flight, `':drag-over'` is set on the node under the
263
+ pointer **and on its ancestors** — the same ancestor-path rule `:hover`
264
+ follows, resolved the same way (a repaint, no React render). For most
265
+ dropzones that is the whole highlight feature, with no handler and no
266
+ state.
267
+
268
+ One thing it does **not** do: it is not filtered by `dropAccept`. A node
269
+ lights up because the pointer is over it, not because it would take the
270
+ drop, so a zone that accepts only `image/png` still highlights while a text
271
+ drag passes over. Where the difference matters — showing "yes" versus "no"
272
+ feedback — use `useDropTarget` and style from `isAccepted`, which is
273
+ exactly the distinction it exists to draw:
274
+
275
+ ```jsx
276
+ <box {...dropProps} style={[s.zone, isOver && (isAccepted ? s.yes : s.no)]} />
277
+ ```
278
+
279
+ ### Scrolling while dragging
280
+
281
+ A drag that rests near the top or bottom edge of a scroll container scrolls
282
+ it, so a drop target below the fold can be reached without letting go.
283
+ Nothing opts in: it applies to any scroll container a drag passes over,
284
+ from this application or another one.
285
+
286
+ The viewport it scrolls is the nearest scroll _container_ enclosing the
287
+ pointer, walked out the same way the wheel walks. Something that scrolls
288
+ pixels of its own without being a container — `<textarea>` and anything else
289
+ that answers `canScroll` alone
290
+ ([extending.md](extending.md#scrolling-content-you-painted)) — is
291
+ deliberately excluded: nothing can be dropped into one, so scrolling it
292
+ during a drag would only move content out of reach.
293
+
294
+ Two consequences worth knowing:
295
+
296
+ - **The content moves under a stationary pointer**, so `onDragEnter`,
297
+ `onDragLeave` and `onDragOver` keep firing while it scrolls, and
298
+ `:drag-over` follows. That is the point — the node under the pointer
299
+ really is changing.
300
+ - **Do not `e.freeze()` a zone inside a scroll container.** Freezing tells the
301
+ source to stop sending positions, and for a drag from another
302
+ application those positions are what start the scrolling in the first
303
+ place.
304
+
305
+ ### `useDropTarget`
306
+
307
+ When the _render_ has to change — a hint label, a disabled sibling — the
308
+ hook adds state on top of the same props:
309
+
310
+ ```jsx
311
+ import { useDropTarget } from 'react-x11';
312
+
313
+ const { dropProps, isOver, isAccepted } = useDropTarget({
314
+ accept: ['files'],
315
+ onDrop: (e) => setFiles(e.files),
316
+ });
317
+
318
+ return (
319
+ <box {...dropProps} style={s.zone}>
320
+ <text>{isOver && !isAccepted ? 'not a file drag' : 'drop files here'}</text>
321
+ </box>
322
+ );
323
+ ```
324
+
325
+ `accept` is the hook's name for `dropAccept` (inside a hook called
326
+ `useDropTarget` the shorter word is unambiguous, and it is what
327
+ react-dropzone users expect); `onDragOver`, `onDragEnter` and `onDragLeave`
328
+ pass through. `isOver` is true whenever a drag is over the node,
329
+ `isAccepted` only when it matches.
330
+
331
+ ## Starting drags
332
+
333
+ ```jsx
334
+ <box
335
+ draggable
336
+ dragData={{
337
+ 'text/uri-list': () => `file://${encodeURI(file.path)}\r\n`, // lazy
338
+ 'text/plain': file.path,
339
+ 'application/x-myapp-file': file, // a live object
340
+ }}
341
+ dragActions={['copy', 'move']}
342
+ onDragEnd={(e) => {
343
+ if (e.action === 'move') remove(file.id);
344
+ }}
345
+ style={{
346
+ backgroundColor: '$panel',
347
+ ':dragging': { backgroundColor: '$textMuted' },
348
+ }}
349
+ />
350
+ ```
351
+
352
+ ### `dragData`
353
+
354
+ A map from payload type name to value. What a value may be, and what
355
+ happens to it:
356
+
357
+ | value | in-app drop | dropped on another application |
358
+ | ----------------------- | ------------------------------ | ---------------------------------------------- |
359
+ | a string | handed over as-is | sent as UTF-8 |
360
+ | a `Buffer`/`TypedArray` | handed over as-is | sent as-is (chunked if large) |
361
+ | any other object | **by reference**, on `e.items` | `JSON.stringify`d |
362
+ | a function (thunk) | called on drop | called when the drag first leaves your windows |
363
+
364
+ Thunks are why a payload nobody asks for is never built. Offer
365
+ `'text/uri-list': () => buildUriList(selection)` and the list is assembled
366
+ only when it is needed — at the drop for an in-app drag, and at the moment
367
+ the pointer first leaves your windows for an external one. Note the second:
368
+ crossing the window edge is enough to resolve every thunk, whether or not
369
+ the drag is ever dropped.
370
+
371
+ Type names are yours to choose. Use real MIME types for anything another
372
+ application might want (`text/uri-list` is what every file manager reads),
373
+ and an `application/x-…` name for private in-app payloads.
374
+
375
+ ### `dragActions`
376
+
377
+ Which actions the source offers, preferred first; default `['copy']`. The
378
+ **target** picks one — `e.accept('move')` on its side — and `onDragEnd`
379
+ reports what was actually performed, so `'move'` semantics (remove the
380
+ original) belong in `onDragEnd`, never in `onDragStart`.
381
+
382
+ Across applications only the **first** entry reaches the wire: a foreign
383
+ target is told the one action you prefer, not the list, so
384
+ `dragActions={['copy', 'move']}` does not give a GTK window a copy/move
385
+ choice. In-app targets see the same single requested action on `e.action`
386
+ and can still answer with any of them.
387
+
388
+ Note what this does _not_ include: **there is no modifier-key
389
+ negotiation.** Holding Ctrl or Shift during a drag does not switch copy to
390
+ move the way a desktop file manager does — the requested action is fixed
391
+ when the drag starts, and only the target can change it.
392
+
393
+ Modifier state is readable on the **source** side, where the events carry
394
+ the real X motion event: `e.ctrlKey` and `e.shiftKey` are meaningful in
395
+ `onDrag`. On the **target** side they are always `false` —
396
+ `onDragEnter`/`onDragOver`/`onDragLeave`/`onDrop` are built from a
397
+ synthesized event with no button mask — so a dropzone cannot see what the
398
+ user is holding. Decide the action from what is being dragged and where it
399
+ lands.
400
+
401
+ ### The gesture
402
+
403
+ DOM-shaped, and deliberately so:
404
+
405
+ - A press on a `draggable` node is **still a click** until the pointer
406
+ moves 4px — measured as `|dx| + |dy|`, so a straight move needs the full
407
+ 4 and a diagonal one starts at about 2.8. Buttons inside draggable rows
408
+ keep working.
409
+ - `onDragStart` fires at the threshold. `e.preventDefault()` cancels — the
410
+ gesture continues as ordinary mouse events.
411
+ - `onDrag` fires per pointer position, with `screenX`/`screenY` and
412
+ `accepted` (whether whatever is under the pointer would take it).
413
+ - `onDragEnd` reports `{ action, dropped }`. `action` is `null` when the
414
+ drag ended over nothing or was refused. Read those rather than `source`,
415
+ which on `onDragStart` and `onDragEnd` is always `'internal'`; only
416
+ `onDrag` reports the transport actually carrying the drag.
417
+ - A completed drag **suppresses the click**, as in the DOM.
418
+ - `':dragging'` is set on the source node for the duration.
419
+
420
+ ### `useDragSource` and drag previews
421
+
422
+ XDND has no drag image in the protocol; every toolkit paints its own. In
423
+ react-x11 that is a `<popup>` following the pointer — which means the
424
+ preview is a live React tree rather than a bitmap:
425
+
426
+ ```jsx
427
+ import { useDragSource } from 'react-x11';
428
+
429
+ const { dragProps, isDragging, position } = useDragSource({
430
+ data: { 'text/plain': file.name },
431
+ actions: ['copy', 'move'],
432
+ onDragEnd: (e) => {
433
+ /* … */
434
+ },
435
+ });
436
+
437
+ return (
438
+ <>
439
+ <box {...dragProps} style={s.card} />
440
+ {isDragging && (
441
+ <popup
442
+ dragPreview
443
+ transparent
444
+ x={position.x + 14}
445
+ y={position.y + 14}
446
+ width={180}
447
+ height={34}
448
+ >
449
+ <box style={position.accepted ? s.previewOk : s.previewNo}>
450
+ <text>{file.name}</text>
451
+ </box>
452
+ </popup>
453
+ )}
454
+ </>
455
+ );
456
+ ```
457
+
458
+ `position` is `{ x, y, accepted }` in screen coordinates while dragging and
459
+ `null` otherwise. **`dragPreview` on the `<popup>` is required**: it tells
460
+ the router that this window is the preview, not something under the
461
+ pointer, and without it the drag would immediately land on its own preview.
462
+
463
+ `transparent` is not required, but a preview usually wants it: the popup is
464
+ a real window, so a card with a `borderRadius` on an opaque one shows the
465
+ window's own ground in the four corners the radius gives up. With it, the
466
+ corners are the desktop, antialiased — and where no compositor is running
467
+ the window falls back to filling itself square, which is what it looked
468
+ like anyway. See
469
+ [`transparent`](elements.md#transparent--rounded-corners-and-translucency).
470
+
471
+ ## The two transports
472
+
473
+ One drag session, two ways of moving the payload. Handlers cannot tell them
474
+ apart unless they ask.
475
+
476
+ | | inside the app (`e.source === 'internal'`) | another application (`'external'`) |
477
+ | -------------------- | ------------------------------------------ | ------------------------------------ |
478
+ | X11 traffic | none | XDND messages + a selection transfer |
479
+ | payload | the live value, on `e.items` | serialised, read with `getData` |
480
+ | thunks resolved | at the drop | when the pointer leaves your windows |
481
+ | `e.files` / `e.text` | yes, same shape | yes |
482
+ | latency | no round trips | a few per pointer position |
483
+
484
+ The switch is automatic and reversible: leave your windows and the drag is
485
+ promoted to XDND; come back and it demotes again. Reorderable lists, trees
486
+ and kanban boards therefore cost nothing on the wire, while the _same_
487
+ component can also be dropped into GIMP.
488
+
489
+ On the [cocoa backend](macos.md) the external transport is AppKit's own —
490
+ `NSDraggingSource` and `NSDraggingDestination` on every window — and the
491
+ switch is drawn differently: once your press crosses the threshold, AppKit
492
+ tracks the gesture everywhere, and a drop on one of your own windows comes
493
+ back through that window's destination with the payload still by reference
494
+ (`e.source === 'internal'`, `e.items` live), so the table above holds. What
495
+ another application reads is the pasteboard, where a `dragData` thunk is a
496
+ promise the system asks for only when a consumer actually reads that type.
497
+
498
+ ## Interoperating with other applications
499
+
500
+ What arrives from the desktop, and what to accept for it:
501
+
502
+ | dragging from | offers | accept with |
503
+ | --------------------------- | -------------------------------------------------------- | ----------- |
504
+ | Nautilus / Thunar / Dolphin | `text/uri-list`, `text/plain` | `['files']` |
505
+ | a GTK editor's selection | `text/plain;charset=utf-8`, `UTF8_STRING`, `STRING` | `['text']` |
506
+ | Firefox (a link) | `text/x-moz-url` (UTF-16), `text/uri-list`, `text/plain` | `['uris']` |
507
+ | Chromium (a link) | `text/uri-list`, `_NETSCAPE_URL`, `text/html` | `['uris']` |
508
+ | another react-x11 app | whatever its `dragData` declared | those names |
509
+
510
+ Dragging **out** works the same way: offer `text/uri-list` and file
511
+ managers, editors and upload widgets will take it.
512
+
513
+ Platform notes:
514
+
515
+ - **Linux / XWayland** — works both directions, including to and from
516
+ native Wayland applications, which is what makes this worth having on a
517
+ modern desktop.
518
+ - **The Wayland backend** — works both directions too, over
519
+ `wl_data_device` (`src/wayland/dnd.js`): a drag from another application
520
+ arrives as the same `DropSession` the X11 path drives, and a drag out is a
521
+ `wl_data_source` the compositor carries from the press serial on. As on
522
+ the cocoa backend, the payload is read at the drop.
523
+ - **Windows, the win32 backend** — works both directions with the desktop,
524
+ over OLE: `IDropTarget` on every window with a `dropAccept` under it,
525
+ `DoDragDrop` out of one. Files arrive as CF_HDROP and become one
526
+ `text/uri-list`, text as CF_UNICODETEXT, and anything else as a clipboard
527
+ format registered under its MIME name — which two react-x11 apps agree on
528
+ for free and nothing else will, exactly as an X11 atom behaves.
529
+
530
+ Two things differ. A `dragData` **thunk is not lazy**: every type's bytes
531
+ are resolved when the drag starts, because a data object that asked JS for
532
+ them from inside `DoDragDrop`'s modal loop would be reaching across the
533
+ bridge's thread split at the one moment its UI thread cannot wait. And the
534
+ shell's question "may this be dropped here" is answered one motion late —
535
+ it picks a cursor, and the bridge returns the previous answer rather than
536
+ blocking. The drop itself is answered exactly, because whether it was a
537
+ copy or a move decides whether the source deletes its original.
538
+
539
+ - **macOS, the cocoa backend** — works both directions with the desktop:
540
+ files from the Finder arrive as `['files']` (each file is a pasteboard
541
+ item of one `public.file-url`; they become one `text/uri-list`), text from
542
+ any app as `['text']`, a URL from Safari as `['uris']`. Pasteboard types
543
+ are UTIs, and react-x11's MIME vocabulary maps onto them through the OS's
544
+ own type database — a private `application/x-myapp-…` travels between two
545
+ react-x11 apps as a `dyn.*` type every process computes alike. Two things
546
+ differ from X11. The payload is read **at the drop** (the pasteboard is the
547
+ source's promise, and it may withdraw it once its session ends), so
548
+ `getData()` answers from that read. And the gesture runs inside AppKit's
549
+ own tracking loop, which owns the thread from the threshold to the
550
+ release: no timer of ours fires and no frame clock ticks in between, so
551
+ every frame of a drag is painted from the callback that reported it, and
552
+ every render a drag schedules — a `<popup dragPreview>` following
553
+ `useDragSource().position`, a `useDropTarget` hint, an insertion marker —
554
+ is dispatched at discrete priority and landed there. The preview follows
555
+ the pointer as it does on X11, and because a `<popup>` here is a
556
+ non-activating panel at the pop-up-menu window level — above every other
557
+ application's windows, which sit at the normal level — it is what the
558
+ desktop sees while the pointer is over Finder or a browser. It is also
559
+ the first window the window server finds when the pointer comes back
560
+ over one of yours, and AppKit does not look past it: a window with no
561
+ dragged types registered is still the one found, the drag then has no
562
+ destination, and the window beneath never hears of it. So the popup is
563
+ made transparent to the pointer — `dragPreview` sets the bridge's
564
+ `ignoresMouseEvents` (@windowkit/appkit 0.6.0) — and the hit passes
565
+ through it to the window it covers: the same exclusion the router makes
566
+ on X11, made where the window server can see it. It registers no
567
+ pasteboard types either, having nothing to accept with. What AppKit's own
568
+ session carries is still a blank image: the drag has no bitmap of the
569
+ system's, only the preview you draw. For the same reason the session's
570
+ slide-back is off: left on, AppKit animates that blank image home over
571
+ about a second _before_ it reports the release, so a drop nothing took
572
+ reached `onDragEnd` — and the preview stayed frozen where the pointer let
573
+ go — only after a pause spent animating nothing. The release ends the
574
+ gesture the moment it happens, as on X11, and a return animation is the
575
+ app's to draw from `onDragEnd`.
576
+ - **macOS / XQuartz** — X client to X client works. Dragging from **Finder**
577
+ into an X11 window does not, and never has:
578
+ [XQuartz#173](https://github.com/XQuartz/XQuartz/issues/173). That is the
579
+ X server's gap, not react-x11's; test against another X client — or run
580
+ the cocoa backend, which has the Finder.
581
+ - **Over `ssh -X`** — each pointer position during an external drag costs a
582
+ few round trips while resolving the foreign window under the cursor, so a
583
+ drag feels heavier on a link than a click does. In-app drags cost
584
+ nothing extra. See [remote.md](remote.md).
585
+
586
+ ## Testing
587
+
588
+ Drags are drivable through `react-x11/test` — real pointer events into the
589
+ in-process X server, no display needed. The threshold is real, so move
590
+ before releasing:
591
+
592
+ ```js
593
+ import { renderX11, fireEvent, act, cleanup } from 'react-x11/test';
594
+
595
+ const { windowNode } = await renderX11(<Board />);
596
+ const card = /* the draggable node */;
597
+ const bin = /* the dropzone node */;
598
+
599
+ await act(async () => {
600
+ fireEvent.mouseDown(card);
601
+ fireEvent.mouseMove(card, { dx: 8 }); // past the threshold, still on the card
602
+ fireEvent.mouseMove(bin); // over the target
603
+ fireEvent.mouseUp(bin);
604
+ });
605
+ ```
606
+
607
+ `onDragStart`, `onDrop` and `onDragEnd` all run, and `e.items` carries the
608
+ live payload. Drops from _other_ applications need a second X connection
609
+ playing the foreign source — `test/dnd.test.js` in this repo does exactly
610
+ that if you need the pattern. See [testing.md](testing.md).
611
+
612
+ ## Limits and defaults
613
+
614
+ | | |
615
+ | ---------------------------------- | ------------------------------------------------------ |
616
+ | drag threshold | 4px |
617
+ | `onDrop` reply watchdog | 10 s |
618
+ | wait for the target's confirmation | 5 s |
619
+ | auto-scroll edge band | 24px, stepping every 30 ms, up to 14px a step |
620
+ | XDND protocol version | 5 (the current one; older sources are negotiated down) |
621
+ | requires | ntk ≥ 5.4.0 |
622
+
623
+ Not supported, deliberately or not yet:
624
+
625
+ - **Motif drag and drop**, the pre-XDND protocol. Some legacy Motif and
626
+ AWT applications speak only that and will not interoperate.
627
+ - **XdndDirectSave (XDS)** — "drag out of the app to save a file into the
628
+ file manager". A separate protocol layered on XDND.
629
+ - **A built-in menu for `XdndActionAsk`.** The choices are handed to you —
630
+ see [Sources that ask](#sources-that-ask) — but the menu itself is yours
631
+ to draw. A built-in one would have to hold the reply while it was open,
632
+ and the reply is what keeps another application's drag from freezing.
633
+ - **`react-dnd`** and other DOM drag libraries — see
634
+ [ecosystem.md](ecosystem.md).
635
+
636
+ ## Protocol notes
637
+
638
+ For anyone watching the wire, or debugging against another toolkit:
639
+
640
+ - `XdndAware` (version 5) is written on every **top-level** window
641
+ unconditionally at realize time. Child windows — `<glarea>`, a nested
642
+ `<window>` — never advertise, per XDND v3+; drags over them arrive at the
643
+ top-level and are routed down in JavaScript. A `<popup dragPreview>`
644
+ never advertises either: it follows the pointer, and a window that is
645
+ never under it has nothing to accept.
646
+ - A window with **no drop targets mounted** answers with a refusal covering
647
+ the whole window, so a well-behaved source stops asking while the pointer
648
+ stays inside it. What an app that never uses drag and drop still pays:
649
+ the `XdndAware` property, one batch of atom interns, and a session object
650
+ per top-level window, all once at realize — plus one refusal per drag
651
+ that crosses it.
652
+ - The drop conversion uses the timestamp from `XdndDrop`, as the protocol
653
+ requires, and `XdndSelection` is released at drag end with the timestamp
654
+ it was acquired with (ICCCM 2.3.1).
655
+ - As a source, the foreign window under the pointer is resolved per motion
656
+ frame rather than from a cache built at drag start — correct, and a few
657
+ round trips heavier on a remote display.
658
+
659
+ The reasoning behind each of those, and the measurements that produced
660
+ them, are in [architecture/drag-and-drop.md](architecture/drag-and-drop.md).
661
+ Runnable demonstrations of both halves, as two separate processes, are
662
+ [`examples/dnd-source.jsx`](../examples/dnd-source.jsx) and
663
+ [`examples/dnd-target.jsx`](../examples/dnd-target.jsx).