react-x11 2.51.1 → 2.53.0

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