react-x11 2.52.0 → 2.54.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 (115) 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 +1096 -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/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
@@ -1,941 +0,0 @@
1
- # Drag and drop: XDND on both sides, and what API it deserves
2
-
3
- Design review of [#126](https://github.com/sidorares/react-x11/issues/126)
4
- ("XDND drop target: accept drags from file managers"), extended to cover the
5
- drag **source** half, and turned into an implementation plan.
6
-
7
- Three questions drive it:
8
-
9
- 1. Is the API proposed in #126 the right one? (lenses: DOM familiarity,
10
- interop with real XDND implementations, reuse of the npm React ecosystem)
11
- 2. react-x11 draws windowless controls into one window canvas. XDND is a
12
- protocol between **X windows**. Where does the advertisement live, and
13
- should it be lazy?
14
- 3. What does adding the source side cost, and how do the two halves share
15
- machinery?
16
-
17
- Checked at react-x11 `d5257ce` (master, 2026-08-02), **ntk 5.3.0**, node-x11
18
- as vendored. Protocol claims are checked against two independent mirrors of
19
- the XDND spec (see [§11](#11-sources)); every code reference is a real line
20
- at that SHA.
21
-
22
- > **Revision note.** The first draft was written against ntk 4.3.0. ntk 5.0.0
23
- > – 5.3.0 landed while it was in review and changed four of its conclusions —
24
- > one of them the headline blocker on the drag-source half. [§0.1](#01-audit-against-ntk-530)
25
- > is the audit; the body below is corrected, not annotated.
26
- >
27
- > **What shipped, and where it diverged.** This is the design as argued, kept
28
- > as the record of _why_; [drag-and-drop.md](../drag-and-drop.md) is the
29
- > reference for what the API actually is. Phases 0–3 shipped in #162 and
30
- > #165. Four things landed differently from the sketch below, and the
31
- > reference page is authoritative on all of them: the floor is **ntk 5.4.0**,
32
- > not 5.3.0 (#167 needed `clipboard.read({ time })` and `clipboard.clear`);
33
- > the foreign target is resolved **per motion, uncached** — the drag-start
34
- > `QueryTree` cache in §6.3/§7 is still the remote-X follow-up; the drag
35
- > rides the **implicit button grab** rather than `GrabPointer` or
36
- > `capturePointer`, swapping only the cursor; and `dragPreview` shipped as a
37
- > **marker prop on a `<popup>` the app renders itself**, not as a node-valued
38
- > prop on the source. The atom interning is also unconditional at realize
39
- > rather than lazy.
40
-
41
- ---
42
-
43
- ## 0. TL;DR
44
-
45
- - **The protocol reading in #126 is sound.** Message list, field layout,
46
- timestamp rule and the "reuse the clipboard read path" instinct all hold up.
47
- The gaps are not in the protocol, they are in the API and in the source half.
48
- - **`XdndAware` should be written unconditionally at realize time, on
49
- top-level windows only** — #126 is right, and for a stronger reason than it
50
- gives. It is a 4-byte property on a window that already exists; it creates
51
- no X windows; and since XDND v3 the spec puts it on top-levels _only_, so
52
- child windows — `<glarea>`, nested `<window>`, and any future windowed
53
- region from [windowed-regions.md](windowed-regions.md) — need nothing at
54
- all. **XDND is orthogonal to the windowed/windowless split.**
55
- - **The lazy refcount you want is real, but it should gate _behaviour_, not
56
- the property.** The protocol has its own lazy channel: the _suppression
57
- rectangle_ in `XdndStatus`. A window with zero live drop targets answers the
58
- first `XdndPosition` with `accept=0` and a rectangle covering the whole
59
- window, and the source sends nothing more until the pointer leaves — one
60
- message per window-entry for the entire drag. That beats de-advertising on
61
- every axis: no property churn per commit, no mid-drag race, no
62
- Offscreen/Suspense bookkeeping.
63
- - **Drop the marker-props design.** #126 proposes `useDropTarget()` returning
64
- opaque props carrying "an internal marker the drop router hit-tests for".
65
- Not needed: `EventManager.dispatch()` already does hit test → path →
66
- capture/target/bubble, keyed on ordinary `on*` props
67
- ([events.js:218](../../src/events.js)). Drop props should be ordinary host
68
- props like every other event in the toolkit, with `useDropTarget` as a
69
- thin convenience in `src/components/` — the same two-layer shape as
70
- `anchorRect`/`useAnchor` and `windowIdOf`/`useWindowId`.
71
- - **`dropAccept` must be declarative, and that is a latency argument, not a
72
- taste argument.** With `dropAccept` as data, `XdndStatus` can be answered from the
73
- ClientMessage handler without entering React. An always-advertised window
74
- that stalls on a render stalls _the source application's drag cursor_ — the
75
- user sees another app freeze. Declarative accept is what makes "always
76
- advertise" safe.
77
- - **Do not ship a fake `e.dataTransfer`.** X selections are asynchronous;
78
- DOM's `getData()` is synchronous. A `dataTransfer.getData()` returning a
79
- Promise fails silently into `"[object Promise]"`. Flatten the payload onto
80
- the event, name the async parts async, and say why in the docs.
81
- - **The two sides should share one drag session with two transports.** In-app
82
- drags (reorderable lists, trees, kanban — the overwhelming majority) never
83
- touch the wire: mousedown threshold, `capturePointer`, synthetic dispatch,
84
- live JS payloads. XDND is engaged only when the pointer leaves our windows,
85
- or when a foreign drag enters. Same handlers either way; `e.source` tells
86
- them apart. This is how GTK and Qt are built, and it is the only shape that
87
- respects the project's perceived-latency goal.
88
- - **The source half is 3–4× the target half, but it is no longer blocked.**
89
- ntk 5.3.0 landed INCR on the selection _write_ side plus arbitrary targets
90
- and binary payloads ([ntk#164](https://github.com/sidorares/ntk/issues/164)),
91
- which was the one hard prerequisite. `app.clipboard.write({'text/uri-list':
92
- …, 'image/png': buf}, { selection: 'XdndSelection', time })` is the whole
93
- data-publishing half of an XDND source, today. See [§0.1](#01-audit-against-ntk-530).
94
- - **npm reuse: `react-dnd` is the one real prize.** Its core (`dnd-core`) is
95
- deliberately DOM-free and its backends are pluggable, so a
96
- `react-x11` backend brings `useDrag`/`useDrop` and the sortable/kanban
97
- ecosystem along. `@dnd-kit` and `react-dropzone` are DOM-bound and stay
98
- unusable — but react-dropzone's _hook shape_ is what React developers know
99
- for file drops, and copying it costs nothing.
100
- - **Testing is unusually good here, and macOS is unusually bad.** node-x11's
101
- in-process JS server implements `SendEvent`, `SetSelectionOwner`,
102
- `ConvertSelection`, `GetSelectionOwner`, `ChangeProperty`, `GetProperty`,
103
- `QueryTree`, `TranslateCoordinates` and `GrabPointer` — so both halves are
104
- testable hermetically with two connections, the `test/wm.test.js` pattern.
105
- Meanwhile **XQuartz does not bridge Finder drags into X11**
106
- ([XQuartz#173](https://github.com/XQuartz/XQuartz/issues/173)), so the
107
- headline use case cannot be demoed on this machine against Finder — only
108
- X-client to X-client.
109
-
110
- ### 0.1 Audit against ntk 5.3.0
111
-
112
- ntk went 4.3.0 → 5.3.0 in two days
113
- ([5.0.0](https://github.com/sidorares/ntk/releases/tag/v5.0.0) …
114
- [5.3.0](https://github.com/sidorares/ntk/releases/tag/v5.3.0)). Four
115
- conclusions moved.
116
-
117
- #### Reversed — the drag-source blocker is gone
118
-
119
- `feat(clipboard): INCR on the write side, required targets, any format`
120
- ([ntk#164](https://github.com/sidorares/ntk/issues/164), 5.3.0) deletes the
121
- "file this before designing around it" item outright. `Clipboard` now:
122
-
123
- - serves **arbitrary target names with binary payloads** —
124
- `write({ 'text/uri-list': str, 'image/png': buf })`, strings encoded UTF-8
125
- (latin-1 for `STRING`), Buffers/TypedArrays as-is;
126
- - transfers **incrementally in both directions** (ICCCM 2.7.2), so the ~256 KB
127
- single-request cap no longer bounds a payload;
128
- - answers the three targets ICCCM 2.6.2 makes mandatory — `TARGETS`,
129
- `TIMESTAMP`, `MULTIPLE` — which a v1 draft would have got wrong;
130
- - takes an **ICCCM-correct timestamp**, `write(data, { time })`, explicitly
131
- never `CurrentTime`. That is the same discipline XDND demands, already
132
- enforced;
133
- - reads back symmetrically: `targets()` enumerates offered type names, and
134
- `read({ target })` returns **raw bytes**, which is exactly the shape
135
- `e.getData(type)` needs.
136
-
137
- `selection` has always been a free-form atom name, so `'XdndSelection'` needs
138
- no new API at all. And the intent is explicit upstream — `clipboard.js:17`
139
- reads, in the module header, that the target-map machinery exists so "HTML,
140
- images and — later — XDND drags are the same machinery."
141
-
142
- **Consequence for the plan:** Phase 0's two clipboard items collapse. The
143
- "factor selection transfer out of `Clipboard` into a reusable primitive" ask
144
- from #126 is satisfied by `targets()` + `read({ target })` + `write(map)`; no
145
- ntk refactor is needed for either half, and Phase 3 stops being gated on an
146
- ntk release cycle.
147
-
148
- #### Improved — `on('close')` frees `on('message')` for XDND
149
-
150
- `feat: a cancellable 'close' event for WM_DELETE_WINDOW`
151
- ([ntk#155](https://github.com/sidorares/ntk/issues/155), 5.3.0). Listening for
152
- `'close'` self-arms: it interns `WM_PROTOCOLS`, calls
153
- `addProtocol('WM_DELETE_WINDOW')`, decodes the ClientMessage, and hands over an
154
- event with `preventDefault()` (`window.js:543`, `window.js:2467`).
155
-
156
- react-x11 still does all of that by hand — `setActions()`, an `InternAtom`
157
- callback, and a `wnd.on('message')` filter
158
- ([nodes/window/listeners.js](../../src/nodes/window/listeners.js)). So the Phase 0 item is now better
159
- than "make the listener unconditional and route by `message_type`": **adopt
160
- `wnd.on('close')` and delete the block**, which leaves `on('message')`
161
- completely free for the XDND router with no routing logic at all. `'message'`
162
- is still emitted for every ClientMessage (`window.js:511`), and
163
- `_emitCloseRequest` runs after it, so the two never contend. Independently
164
- worth doing: `onCloseRequest` gains cancellability.
165
-
166
- `Window` also grew the general helpers this needs — `addProtocol` /
167
- `removeProtocol` (`window.js:1459`) and `atom` / `getProperty` / `setProperty`
168
- / `deleteProperty` (`window.js:2100`) — so `XdndAware` is
169
- `wnd.setProperty('XdndAware', [5], { type: 'ATOM' })` rather than a raw
170
- `ChangeProperty`.
171
-
172
- #### Weakened — the pointer-grab item is optional, not a prerequisite
173
-
174
- `grabPointer({ cursor })` takes a cursor **XID**, and `app.cursors.get(name)`
175
- mints one from the standard cursor font (`cursor.js:68`). So the drag cursor
176
- _at grab time_ needs no new ntk API. Only the mid-drag swap as the accepted
177
- action changes needs `ChangeActivePointerGrab`, which node-x11 exposes
178
- (`corereqs.js:867`) and react-x11 can call directly. Phase 0 item downgraded
179
- from prerequisite to optional convenience wrapper.
180
-
181
- New caveat in the same area: `cursorShapes` is the X **core cursor font**, so
182
- the themed XCursor names toolkits use for drag feedback (`dnd-copy`,
183
- `dnd-move`, `dnd-none`) are not loadable. Feedback will be a core-font
184
- approximation until ntk gains XCursor theme loading — cosmetic, but it is the
185
- part of a drag the user stares at.
186
-
187
- #### Strengthened — request buffering does _not_ cost the latency argument
188
-
189
- `feat: one socket write per frame — buffer requests by default`
190
- ([ntk#141](https://github.com/sidorares/ntk/issues/141), 5.0.0) is the change
191
- that looked most likely to undercut §2.4's "answer `XdndStatus` in the same
192
- event-loop turn": a 64 KB output buffer with a 5 ms age gate would have added
193
- up to a frame of latency to every reply.
194
-
195
- It does not. node-x11's frame buffer has an explicit backstop —
196
- "_nothing is left in the buffer when the event loop goes to poll for I/O_",
197
- implemented as a `setImmediate` flush (`framebuffer.js:33-38`), alongside the
198
- size cap, the 5 ms age gate and a flush before any request expecting a reply.
199
- A `SendEvent` written from the ClientMessage handler therefore leaves the
200
- socket at the end of the current phase, before the next poll — and now as part
201
- of one write rather than one syscall per request. The claim survives with a
202
- named mechanism, and the drag path got slightly cheaper.
203
-
204
- #### Two new caveats for the source half
205
-
206
- 1. **`write()` payloads are eager.** `_encode()` serialises everything at
207
- `write()` time (`clipboard.js:398`), so §6.2's lazy `dragData` thunks are
208
- resolved by react-x11 **at promotion** — when it takes `XdndSelection` —
209
- not when a target converts. Still worth having (an in-app drag serialises
210
- _nothing_, and an external drag serialises once at promotion rather than at
211
- mousedown), but the "pull-based, so a 20 MB payload is never built for a
212
- drag that ends in the wastebasket" claim is now only true of drags that
213
- never leave our windows. Ask ntk for lazy payload getters if that matters.
214
- 2. **Ownership sits on ntk's private helper window.** `Clipboard` owns every
215
- selection on its hidden 1×1 window, not on the window we would name as the
216
- XDND source. This is protocol-legal — the target converts `XdndSelection`
217
- and the server routes to whoever owns it, wherever that is — and none of
218
- the reference implementations validate owner == source window. Verify in
219
- Phase 3; if a target does check, ntk needs a `window` option on `write()`.
220
- Related: there was no disown API when this was written, so releasing
221
- `XdndSelection` at drag end meant `app.X.SetSelectionOwner(0, sel, time)`
222
- directly. **ntk 5.4.0 added `clipboard.clear(selection)`**
223
- ([ntk#169](https://github.com/sidorares/ntk/issues/169)) and `_disown()`
224
- uses it.
225
-
226
- #### Unchanged
227
-
228
- Everything in §2 (advertisement, refcount, lifecycle), §3–§5 (the three
229
- lenses), §6 (the API), and §8 (the test plan) is unaffected: ntk 5.x adds no
230
- XDND support, no window-property policy changes, and no event-routing changes
231
- that touch them. `SendClientMessage` (node-x11, `xcore.js:246`, whose own
232
- comment names XDND) is still the send path.
233
-
234
- ---
235
-
236
- ## 1. What #126 gets right
237
-
238
- Worth stating, because the rest of this document is criticism and the
239
- foundation is good.
240
-
241
- - **Message set and field layout are correct.** `XdndEnter` bit 0 →
242
- `XdndTypeList`, types in `l[2..4]`; `XdndPosition` root coordinates packed
243
- `(x << 16) | y` in `l[2]`; `XdndStatus` accept bit and rectangle;
244
- `XdndDrop`'s timestamp in `l[2]`. Verified field-by-field against two spec
245
- mirrors.
246
- - **The timestamp rule is called out** — `ConvertSelection` must carry
247
- `XdndDrop`'s `l[2]`, not `CurrentTime`. This is the single most commonly
248
- botched part of XDND and #126 has it.
249
- - **"Advertise unconditionally, answer _not accepting_"** is the right call
250
- (§2 sharpens the argument).
251
- - **Reusing the clipboard read path** is right — and as of ntk 5.3.0 the
252
- public API is already the right shape: `targets()` enumerates offered type
253
- names and `read({ selection: 'XdndSelection', target })` returns raw bytes,
254
- INCR reassembly included (`clipboard.js:127`, `clipboard.js:148`).
255
- - **Splitting the source off as separate work** is right. It is a much bigger
256
- job (§6), and the target half is independently useful.
257
- - **The infrastructure survey is accurate**: ClientMessage already routes to
258
- `WindowNode` ([nodes/window/listeners.js](../../src/nodes/window/listeners.js)), hit testing and hover
259
- path diffing already exist in `EventManager`, and `window._screenOrigin`
260
- ([nodes/window/window.js](../../src/nodes/window/window.js)) already holds the root-coordinate
261
- translation `XdndPosition` needs.
262
-
263
- Four corrections to the protocol detail before moving on:
264
-
265
- 1. **`XdndStatus` `l[1]` has a second bit.** Bit 1 means _"send
266
- `XdndPosition` even while inside the rectangle"_. #126 mentions only the
267
- accept bit. This bit is the difference between a dropzone that can draw an
268
- insertion caret and one that goes deaf — see §2.4.
269
- 2. **The rectangle is not a free optimisation.** #126 proposes replying with
270
- "the hit node's screen rect" on every position. For a uniform dropzone that
271
- is right and saves the source a message per motion frame; for a list that
272
- needs an insertion index, or a zone with edge auto-scroll, it silently
273
- breaks the feature. Default must be _no suppression_; suppression is opt-in
274
- (`e.freeze()`), with the one automatic exception in §2.3.
275
- 3. **`XdndFinished` needs a watchdog, not just a reply.** If `onDrop` awaits
276
- something slow, the source blocks — that is another application's UI
277
- frozen by ours. Send `XdndFinished` when the handler settles _or_ after a
278
- timeout, whichever comes first, and report the timeout to the app.
279
- 4. **Version negotiation must be stored.** `XdndEnter`'s `l[1]` high byte
280
- carries the source's version; `XdndFinished`'s flags in `l[1]`/`l[2]` are
281
- v5-only. Cap replies to `min(5, sourceVersion)` and keep the source window
282
- id — every reply is addressed to it, not to whoever the pointer is over.
283
-
284
- ---
285
-
286
- ## 2. Where the advertisement lives (the windowless question)
287
-
288
- > _"our toolkit renders windowless controls inside window canvas… we need
289
- > lazily Advertise on a nearest parent windowed control / De-advertise in the
290
- > other direction whenever a DnD control is mounted / unmounted."_
291
-
292
- The instinct is right — there **is** something to refcount — but the thing to
293
- gate is not `XdndAware`.
294
-
295
- ### 2.1 `XdndAware` costs no windows
296
-
297
- The concern behind the question is X window economy, which this project takes
298
- seriously and rightly ([windowed-regions.md](windowed-regions.md) §3.2 is
299
- where window-per-control dies). `XdndAware` is not that. It is:
300
-
301
- - **a property, not a window** — zero `CreateWindow`, zero map/move storms,
302
- zero server-side pick-tree cost;
303
- - **one `ChangeProperty`**, 4 bytes of payload, no round trip once the atom is
304
- interned;
305
- - **on a window that exists anyway** — the top-level `<window>`.
306
-
307
- `WindowNode.realize()` already sends `WM_NAME`, `_NET_WM_NAME`, `WM_CLASS`,
308
- `_NET_WM_PID`, size hints, and optionally `WM_PROTOCOLS`, `WM_TRANSIENT_FOR`
309
- and `_NET_WM_STATE`. One more 32-bit property is well under 5% of a window's
310
- setup traffic and is sent once per window, ever.
311
-
312
- ### 2.2 The spec puts it on top-levels, which makes windowed regions a non-issue
313
-
314
- XDND v3 moved `XdndAware` to **top-level windows only**; sources locate the
315
- target by descending the tree with `TranslateCoordinates` and then looking for
316
- `XdndAware` on the top-level (GTK and Qt both resolve the client window
317
- through `WM_STATE` and walk up).
318
-
319
- Consequences, and this is the direct answer to the question:
320
-
321
- - **`<glarea>`'s child window: nothing.** A drag over a `<glarea>` is found
322
- through the enclosing top-level and routed by our own hit test.
323
- - **A future windowed a scrolling `<box>` pane, or any `<box windowed>` that never
324
- ships: nothing.** The promotion decision in
325
- [windowed-regions.md](windowed-regions.md) §8 is invisible to XDND. That is
326
- a genuine argument _for_ that document's verdict — a feature that would have
327
- had to care about `windowed` does not.
328
- - **Nested `<window>` children need routing, not advertising.** react-x11
329
- supports child `<window>`s inside a parent
330
- ([nodes/window/window.js](../../src/nodes/window/window.js)). Those are not top-levels, so they get
331
- no property; messages arrive at the outer window carrying **root**
332
- coordinates, and the router must descend our own window tree in JS:
333
- root point → each realized `WindowNode`'s `_screenOrigin` + size → topmost
334
- match in `_xStack` order → that node's `hitTest`. Cheap, local, no round
335
- trips, and it is the same walk `anchorRect` already does in reverse.
336
- - **`<popup>` is a top-level** (override-redirect, child of root) and _does_
337
- get the property, so a drop onto an open menu or a combo panel works.
338
-
339
- ### 2.3 What the refcount should actually gate
340
-
341
- Keep a `Set` of registered drop-target nodes **per `WindowNode`** (a `<popup>`
342
- is its own root, so it gets its own). It gates three things, none of them the
343
- property:
344
-
345
- 1. **The whole-window suppression fast path.** `set.size === 0` → reply to the
346
- first `XdndPosition` with `accept=0`, bit 1 clear, and a rectangle covering
347
- the whole window in root coordinates. The source stops sending positions
348
- until the pointer leaves. **One ClientMessage per window-entry, for the
349
- whole drag.** This is exactly the saving de-advertising would have bought,
350
- obtained through the protocol's own mechanism.
351
- 2. **Atom interning.** Intern the ~15 `Xdnd*` atoms lazily, on the first
352
- drop-target registration _or_ the first `XdndEnter`, whichever comes first.
353
- An app with no DnD never sends the `InternAtom` batch.
354
- 3. **Module activation.** `src/dnd.js` stays cold. `realize()` writes one
355
- property; the shared `message` listener is an atom range check.
356
-
357
- ### 2.4 Why _not_ to make the property lazy
358
-
359
- Four reasons, in order of how much they hurt:
360
-
361
- 1. **Mid-drag mounts are invisible.** GTK's source builds its window cache at
362
- _drag start_ — before our app can possibly know a drag exists. A dropzone
363
- that mounts in reaction to app state during a drag would never be seen by
364
- that source, and the failure is silent and unreproducible.
365
- 2. **Commit churn.** Route changes, tab switches and virtualised lists mount
366
- and unmount dropzones constantly. A 1 → 0 → 1 refcount across a commit
367
- boundary costs `DeleteProperty` + `ChangeProperty` _per commit_, which is
368
- more traffic than the one write it was trying to avoid.
369
- 3. **Offscreen and Suspense.** `hideInstance` does not unmount
370
- ([Reconciler.js:408](../../src/Reconciler.js)), so a correct refcount would
371
- need hidden-tracking too — state that is easy to get subtly wrong and whose
372
- failure mode is "drops stopped working in this tab".
373
- 4. **There is no user-visible difference.** Not-advertised and
374
- advertised-but-rejecting both produce the source's "no drop" cursor. The
375
- lazy version buys nothing the user can see, and costs the three failures
376
- above.
377
-
378
- The one obligation `always advertise` creates: **you must always answer, and
379
- answer fast.** A window that is `XdndAware` and silent freezes the _source_
380
- application's drag. That obligation is why `dropAccept` has to be declarative
381
- (§4.1) — the answer must be computable without waiting for React.
382
-
383
- ### 2.5 The exact lifecycle points
384
-
385
- The codebase already answers this; `trapFocus` is the identical problem
386
- (subtree-scoped declaration, root-scoped registry) and its wiring is the
387
- template:
388
-
389
- | Moment | Hook | What to do |
390
- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
391
- | Mount | `finalizeInitialChildren` returns `true` when the props declare drop handling ([Reconciler.js:342](../../src/Reconciler.js)) → `commitMount` ([Reconciler.js:352](../../src/Reconciler.js)) | `root._dnd.register(node)`. Must be `commitMount`, not `createInstance`: the node needs its `root`, which `_setRoot` assigns on insertion. |
392
- | Update | `commitUpdate` → `applyProps` ([nodes/node.js](../../src/nodes/node.js)), next to the existing `trapFocus` branch | register/unregister on the boolean edge only |
393
- | Unmount | `detachDeletedInstance` ([Reconciler.js:424](../../src/Reconciler.js)), which already calls `events.forget(instance)` | one more line: `instance.root?._dnd?.forget(instance)` |
394
- | Hidden | — | do **not** track. `hitTest` already skips hidden nodes; a hidden-but-registered node costs one unsuppressed `XdndStatus` and nothing else. |
395
-
396
- Realize-time (`WindowNode.realize()`, before `map()`): write `XdndAware = 5`
397
- as `ATOM`/32 unconditionally, next to `applyWindowStates` — same reasoning as
398
- the EWMH properties there, a declaration made before anyone can look.
399
-
400
- One refactor falls out: the `message` listener is currently installed only
401
- when `onCloseRequest` is set ([nodes/window/listeners.js](../../src/nodes/window/listeners.js)). Make it
402
- unconditional and route by `ev.message_type` — `WM_PROTOCOLS` to the close
403
- path, `Xdnd*` to the DnD router. Cheap, and `_NET_WM_PING`/`_NET_WM_SYNC` will
404
- want it later.
405
-
406
- ---
407
-
408
- ## 3. Lens 1 — familiarity to the DOM API
409
-
410
- react-x11 tracks the DOM closely and deliberately: capture/bubble
411
- (`onFooCapture` then `onFoo`), `stopPropagation`/`preventDefault`, `detail`
412
- click counting, `capturePointer` modelled on implicit pointer capture. Drag
413
- and drop should inherit that, with three deliberate divergences.
414
-
415
- **Keep, verbatim:**
416
-
417
- - `onDragStart`, `onDrag`, `onDragEnd` (source) and `onDragEnter`,
418
- `onDragOver`, `onDragLeave`, `onDrop` (target). Same names, same
419
- enter/leave-do-not-bubble rule the hover path already implements
420
- ([events.js:428](../../src/events.js)).
421
- - `draggable` as the prop that makes a node a source. It is the HTML
422
- attribute name, and it reads correctly.
423
- - `'copy' | 'move' | 'link'` action names — they map 1:1 onto
424
- `XdndActionCopy`/`Move`/`Link`, which is a gift the DOM's `dropEffect`
425
- vocabulary hands us for free.
426
- - MIME type strings as the type vocabulary.
427
-
428
- **Diverge, on purpose:**
429
-
430
- 1. **No `preventDefault()`-to-accept.** In the DOM, `dragover` +
431
- `preventDefault()` means "suppress the browser's default of refusing the
432
- drop" — the most-complained-about wart in the HTML5 DnD API, and it is
433
- _meaningless here_: there is no browser and no default. Use
434
- `e.accept(action)` / `e.reject()`. Explicit, discoverable, and the
435
- declarative `dropAccept` prop means most code never calls either.
436
- 2. **No `e.dataTransfer`.** X selections are asynchronous — `ConvertSelection`
437
- then wait for `SelectionNotify`, possibly through INCR chunks. DOM's
438
- `getData()` is synchronous and returns a string. A `dataTransfer` whose
439
- `getData()` returns a Promise is a trap that fails _silently_:
440
- `"dropped: " + e.dataTransfer.getData('text/plain')` yields
441
- `"dropped: [object Promise]"` with no error anywhere. Flatten onto the
442
- event, name the async things async (`await e.getData(type)`), and let the
443
- different name carry the different contract.
444
- 3. **Data may be lazy, and may be a live object.** DOM's `setData(type, str)`
445
- is eager and string-only. XDND is pull-based — the target converts the
446
- selection _after_ the drop — so `dragData` accepts thunks, and for in-app
447
- drags it can carry a JS value that is never serialised at all. Strictly
448
- better than the DOM, and it matches `react-dnd`'s model, which is what
449
- React developers actually use.
450
-
451
- ---
452
-
453
- ## 4. Lens 2 — interop with real XDND implementations
454
-
455
- This is where a plausible-looking API quietly fails in the field. Each item
456
- below is a concrete requirement on the API surface, not just the plumbing.
457
-
458
- ### 4.1 `dropAccept` must be smarter than string equality
459
-
460
- The same logical payload arrives under different names depending on who is
461
- dragging:
462
-
463
- | Drag | Types actually offered |
464
- | -------------------------------------- | ------------------------------------------------------------------------------------------ |
465
- | Files from Nautilus / Thunar / Dolphin | `text/uri-list`, `text/plain`, sometimes `x-special/gnome-icon-list` |
466
- | Text from a GTK app | `text/plain;charset=utf-8`, `UTF8_STRING`, `text/plain`, `STRING`, `TEXT`, `COMPOUND_TEXT` |
467
- | A link from Firefox | `text/x-moz-url` (**UTF-16LE**, `url\ntitle`), `text/uri-list`, `text/plain` |
468
- | A link from Chromium | `text/uri-list`, `_NETSCAPE_URL`, `text/html` |
469
-
470
- `dropAccept: ['text/plain']` therefore misses GTK text entirely, and
471
- `dropAccept: ['text/uri-list']` catches files but silently drops Firefox's
472
- preferred link type. So `dropAccept` takes:
473
-
474
- - exact MIME strings — `'image/png'`;
475
- - **semantic groups** — `'files'`, `'text'`, `'uris'` — expanding through a
476
- normalisation table that is part of the library, not of every app;
477
- - a predicate — `(types) => types.some(...)` — for the rest.
478
-
479
- Getting this table right _is_ the interop feature. It is also the one piece
480
- that must be data rather than code, so `XdndStatus` can be answered without
481
- entering React (§2.4).
482
-
483
- ### 4.2 `text/uri-list` must be parsed for the app
484
-
485
- Per RFC 2483 it is CRLF-separated, percent-encoded, and `#`-prefixed lines are
486
- comments. #126's `await e.getData('text/uri-list')` hands back the raw string
487
- and every app writes the same buggy decoder. Provide instead:
488
-
489
- ```js
490
- e.files; // [{ uri: 'file:///home/a/My%20Doc.pdf', path: '/home/a/My Doc.pdf' }]
491
- ```
492
-
493
- `path` is present only for `file:` URIs whose host is empty, `localhost`, or
494
- this machine — a remote `file://otherhost/...` has no local path and must not
495
- pretend to. That rule is exactly the kind of thing that belongs in the library
496
- once.
497
-
498
- ### 4.3 Protocol obligations that shape the API
499
-
500
- - **Answer `XdndPosition` promptly, always.** §2.4. Declarative `dropAccept`
501
- handles the common case without React; a handler-based answer runs at
502
- `DiscreteEventPriority` and, if a render is in flight, replies with the last
503
- known answer rather than waiting.
504
- - **`XdndFinished` must always be sent.** Async `onDrop` is supported (it is
505
- the natural shape — the data arrives asynchronously), so the contract is:
506
- the reply goes out when the returned promise settles, or after
507
- `dropTimeout` (default ~10 s), whichever is first, with a dev warning on
508
- timeout. An app must not be able to hang another app's drag by forgetting an
509
- `await`.
510
- - **`XdndProxy` must be honoured on the source side** — read it on each
511
- candidate window, send to the proxy while keeping the real window id in the
512
- message fields. We never _set_ it, since only top-levels advertise (§2.2).
513
- - **Version negotiation.** Store `sourceVersion` from `XdndEnter`'s high byte;
514
- v5-only `XdndFinished` fields are omitted below 5.
515
- - **`XdndActionAsk`** should eventually pop a menu built from the source's
516
- `XdndActionList` + `XdndActionDescription` — react-x11 already has
517
- `ContextMenu`. v1 narrows to `XdndActionCopy`, which the spec permits.
518
-
519
- ### 4.4 What we will not speak, and should say so
520
-
521
- - **Motif drag-and-drop** (the older protocol). Legacy Motif and some AWT
522
- builds speak only that; they will not interop. Not worth implementing.
523
- - **XdndDirectSave (XDS)** — "drag out of the app to save a file into the file
524
- manager". A separate protocol layered on XDND. Worth a documented "not
525
- supported" line, because its absence looks like a bug.
526
- - **Finder → X11 on macOS.** XQuartz does not bridge native drags into X11
527
- ([XQuartz#173](https://github.com/XQuartz/XQuartz/issues/173)). On this
528
- machine the feature can only be exercised X-client to X-client (a GTK app
529
- under XQuartz, `mwh/dragon`, or the hermetic tests in §8). Under XWayland on
530
- Linux, XDND does bridge to and from native Wayland apps — which is where
531
- #126's impact claim holds.
532
-
533
- ---
534
-
535
- ## 5. Lens 3 — reuse of the npm React ecosystem
536
-
537
- | Package | Status | Why |
538
- | ------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
539
- | **`react-dnd`** | **the prize** | `dnd-core` is deliberately DOM-free and backends are pluggable (constructor takes a manager, plus `setup()`/`teardown()`). A `react-x11` backend brings `useDrag`/`useDrop` and everything built on them. |
540
- | `react-dropzone` | unusable, but _copy its shape_ | Already documented as failing twice ([ecosystem.md](../ecosystem.md)) — `<input>`, then `window is not defined` in its own mount effect. Its hook shape (`getRootProps()`, `isDragActive`, `acceptedFiles`) is nonetheless what React developers know for file drops. |
541
- | `@dnd-kit/core` | unusable | Sensors are pluggable but the core measures with `getBoundingClientRect` and synthesises DOM `PointerEvent`s. A backend is not the seam it offers. |
542
- | `react-beautiful-dnd` / `@hello-pangea/dnd` | unusable | DOM-only by design, and its use case (in-app reorder) is fully served by §6's local path. |
543
-
544
- Two conclusions:
545
-
546
- 1. **Design the low-level API so a `react-dnd` backend is writable on top of
547
- it.** That means the drag session must expose imperative
548
- `beginDrag`/`hover`/`drop`/`endDrag` transitions and node-rect measurement,
549
- not only React props — which it needs internally anyway. Nothing here is
550
- extra work; it is just a constraint on where the seam goes.
551
- `NativeTypes.FILE` maps onto foreign XDND drops; in-app drags never reach
552
- X. The backend can ship later, in a separate package, without changing this
553
- API — as long as the seam exists.
554
- 2. **Match `react-dropzone`'s hook ergonomics** in `useDropTarget`, including
555
- the render state (`isOver`, `isAccepted`), because that is the muscle
556
- memory. But go one better: react-x11 has style states, so the common case
557
- needs no state at all — see `:drag-over` in §6.1.
558
-
559
- ---
560
-
561
- ## 6. The proposed API
562
-
563
- ### 6.1 Target side
564
-
565
- Host props, on any drawn node, dispatched through the existing
566
- capture/bubble machinery — no marker, no parallel router:
567
-
568
- ```jsx
569
- <box
570
- dropAccept={['files']} // declarative; answers XdndStatus without React
571
- onDragEnter={(e) => {}} // does not bubble, like onMouseEnter
572
- onDragOver={(e) => {}} // per motion, unless frozen
573
- onDragLeave={(e) => {}}
574
- onDrop={async (e) => setFiles(e.files)}
575
- style={{
576
- borderColor: '$border',
577
- ':drag-over': { borderColor: '$accent' }, // new style state
578
- }}
579
- />
580
- ```
581
-
582
- **Why `dropAccept` and not `accept`.** Prop _presence_ is what registers a
583
- node as a drop target, so the name carries behaviour, and a name as generic
584
- as `accept` (HTML's `<input accept>`, react-dropzone's option) could ride
585
- in on a `{...spread}` and silently make a box a dropzone — on `<input
586
- type=file>` the element supplies the context; on a generic `<box>` nothing
587
- does. The `drop*` prefix also keeps the family discoverable next to
588
- `onDrop` and mirrors the source side's `drag*` data props (`dragData`,
589
- `dragActions`, `dragPreview`). Rejected alternatives: `dndAcceptTypes`
590
- (abbreviation prefix foreign to the codebase, splits the family into two
591
- namespaces, and `Types` over-narrows a value that can be a group or a
592
- predicate), `acceptDrops` (reads boolean, takes a list), `droppable`
593
- (prettiest symmetry with `draggable`, but the target needs a _filter_, and
594
- with `onDrop` implying accept-anything its boolean form is boilerplate).
595
-
596
- `:drag-over` joins `STATE_KEYS` ([styles.js:156](../../src/styles.js)) and is
597
- set by the router through the same `setStyleState` path `:hover` uses
598
- ([events.js:429](../../src/events.js)). The whole "highlight the dropzone"
599
- feature — the thing every react-dropzone tutorial spends a `useState` on —
600
- becomes styling.
601
-
602
- The event, built by `_makeEvent` ([events.js:178](../../src/events.js)) so it
603
- inherits `x`/`y`/`localX`/`localY`/`stopPropagation` for free:
604
-
605
- ```
606
- e.types string[] normalised type names offered by the source
607
- e.has(type) boolean alias-aware ('text', 'files', or an exact MIME)
608
- e.action 'copy'|'move'|'link'|'ask'|'private' what the source asked for
609
- e.source 'internal'|'external'
610
- e.screenX/Y number root coordinates (XdndPosition gives these natively)
611
- e.accept(action?) default: the source's requested action
612
- e.reject()
613
- e.freeze() opt into the XdndStatus suppression rectangle
614
- for this node's rect (see §1, correction 2)
615
- // onDrop only:
616
- e.getData(type) -> Promise<Buffer|string>
617
- e.files [{ uri, path }] parsed text/uri-list (§4.2)
618
- e.text string the best available text flavour
619
- e.items any live JS payload — internal drags only, no serialisation
620
- ```
621
-
622
- Convenience hook in `src/components/`, mirroring react-dropzone:
623
-
624
- ```jsx
625
- const { dropProps, isOver, isAccepted } = useDropTarget({
626
- accept: ['files'], // the hook name supplies the scope; emits `dropAccept`
627
- onDrop: (e) => setFiles(e.files),
628
- });
629
- <box {...dropProps} style={s.zone} />;
630
- ```
631
-
632
- ### 6.2 Source side
633
-
634
- ```jsx
635
- <box
636
- draggable
637
- dragData={{
638
- 'text/uri-list': () => selected.map((f) => pathToUri(f.path)), // lazy: pull-based protocol
639
- 'text/plain': selected.map((f) => f.name).join('\n'),
640
- 'application/x-myapp-row': { id: row.id }, // in-app: live object
641
- }}
642
- dragActions={['copy', 'move']}
643
- dragPreview={<Chip label={row.name} />} // rendered into a <popup> that follows the pointer
644
- onDragStart={(e) => {
645
- /* e.preventDefault() cancels */
646
- }}
647
- onDrag={(e) => {}}
648
- onDragEnd={(e) => {
649
- if (e.action === 'move') remove(row.id);
650
- }}
651
- style={{ ':dragging': { backgroundColor: '$textMuted' } }}
652
- />
653
- ```
654
-
655
- Three things to note:
656
-
657
- - **`dragPreview` is a React node, not an image.** XDND has no drag image in
658
- the protocol; every toolkit implements it as an override-redirect window
659
- that follows the pointer — and react-x11 already has one, `<popup>`. So the
660
- drag preview is a live component, which beats DOM's
661
- `setDragImage(HTMLImageElement)` outright. The preview window must be
662
- skipped when descending for the target; since we hold the pointer grab, we
663
- are the only client searching, so skipping our own ids is sufficient.
664
- - **Lazy `dragData` defers serialisation to promotion.** A drag that never
665
- leaves our windows serialises nothing at all — it carries the live JS value.
666
- A drag that goes external resolves the thunks once, when it takes
667
- `XdndSelection`, because ntk's `write()` encodes eagerly
668
- ([§0.1](#01-audit-against-ntk-530)). Deferring all the way to the target's
669
- conversion, as the pull-based protocol would allow, needs lazy payload
670
- getters in ntk — worth asking for only if a real payload makes it matter.
671
- - **The gesture is the existing one.** Threshold on mousedown + motion, then
672
- `capturePointer` — the same mechanics `Slider` and `SplitPane` already use
673
- ([events.js:202](../../src/events.js)).
674
-
675
- Hook form: `const { dragProps, isDragging } = useDragSource({ data, actions })`.
676
-
677
- ### 6.3 One session, two transports — how the halves fit together
678
-
679
- A single `DragSession` in `src/dnd.js`, with a phase:
680
-
681
- ```
682
- mousedown on draggable
683
- └─ threshold exceeded ──▶ phase: 'local'
684
- • no grab (the implicit button grab already delivers motion)
685
- • no X traffic at all
686
- • hit test + dispatch('DragOver', …) through EventManager
687
- • payload is the live JS value; e.source === 'internal'
688
- │
689
- ├─ pointer leaves all of our windows ──▶ phase: 'external'
690
- │ • SetSelectionOwner(XdndSelection)
691
- │ • write XdndTypeList if > 3 types
692
- │ • GrabPointer with the action cursor
693
- │ • build the top-level cache (one QueryTree of root + geometry)
694
- │ • XdndEnter / XdndPosition / XdndStatus / XdndDrop / XdndFinished
695
- │
696
- └─ pointer returns over our own window ──▶ XdndLeave, back to 'local'
697
- (selection ownership is kept until drag end — it is free)
698
-
699
- foreign XdndEnter arrives ──▶ same synthetic dispatch, e.source === 'external'
700
- ```
701
-
702
- Why this and not "XDND for everything":
703
-
704
- - **Latency.** In-app reorder is the overwhelmingly common case. Routing it
705
- through a pointer grab, a root-tree descent and a selection round trip would
706
- put wire round trips on the drag path, against the project's stated
707
- perceived-latency ordering.
708
- - **Fidelity.** In-app you can drag a live JS object. Forcing serialisation
709
- through MIME strings for a drag that never leaves the process is pure loss —
710
- and it is precisely why `react-dnd` does not do it.
711
- - **One API.** Handlers cannot tell the transports apart except by asking
712
- (`e.source`), so an app written for in-app reorder gains cross-app drops by
713
- adding a MIME type to `dragData`.
714
-
715
- This is also what makes the two halves cheap together: the source's target
716
- tracking (which node is under the pointer, enter/leave diffing, accept state)
717
- and the target's routing are **the same code**, differing only in whether the
718
- peer is a foreign window or our own subtree.
719
-
720
- ---
721
-
722
- ## 7. Implementation plan
723
-
724
- Dependency-ordered. Phase 1 alone closes #126.
725
-
726
- ### Phase 0 — prerequisites and decisions
727
-
728
- **No ntk release gates any of this** — 5.3.0 already ships what both halves
729
- need ([§0.1](#01-audit-against-ntk-530)).
730
-
731
- - **[react-x11]** Adopt `wnd.on('close')` for `onCloseRequest` and delete the
732
- `setActions()` + `InternAtom` + `on('message')` block
733
- ([nodes/window/listeners.js](../../src/nodes/window/listeners.js)) — filed as
734
- [#160](https://github.com/sidorares/react-x11/issues/160). That frees
735
- `'message'` for the XDND router with no routing logic, and makes
736
- `onCloseRequest` cancellable. The only real prerequisite, and independently
737
- worth doing.
738
- - **[verify in Phase 3]** That foreign targets accept `XdndSelection` owned by
739
- ntk's clipboard helper window rather than by the advertised source window.
740
- If one does not, ntk needs a `window` option on `write()`.
741
- - **[ntk, optional]** `ChangeActivePointerGrab` wrapper for the mid-drag
742
- cursor swap — `grabPointer({ cursor: app.cursors.get(name) })` already
743
- covers grab time, and node-x11 exposes the request, so this is convenience.
744
- - **[ntk, done in 5.4.0]** `clipboard.clear(selection)` to disown
745
- `XdndSelection` at drag end
746
- ([ntk#169](https://github.com/sidorares/ntk/issues/169)), and a `time`
747
- option on `read()`/`targets()` so a drop converts with `XdndDrop`'s
748
- timestamp rather than `CurrentTime`
749
- ([ntk#168](https://github.com/sidorares/ntk/issues/168)). Both are wired
750
- up. Still open: lazy payload getters on `write()`, if eager serialisation
751
- at promotion turns out to matter.
752
- - **[decision]** `:drag-over` (and later `:dragging`) join `STATE_KEYS`.
753
-
754
- ### Phase 1 — drop target (closes #126)
755
-
756
- 1. `src/dnd.js`: atom table (lazy intern), `XdndAware` writer, ClientMessage
757
- decode/encode helpers, version negotiation.
758
- 2. `WindowNode.realize()`: write `XdndAware = 5` before `map()`.
759
- 3. Per-`WindowNode` drop-target registry + the three lifecycle hooks (§2.5).
760
- 4. Root-coordinate router: root point → owning `WindowNode` (descending our
761
- own child windows, §2.2) → `hitTest` → path.
762
- 5. `XdndEnter`/`Position`/`Leave`/`Drop` handling; `XdndStatus` answered from
763
- declarative `dropAccept` without entering React; the zero-target whole-window
764
- suppression fast path; enter/leave diffing reusing the hover-path algorithm
765
- ([events.js:429](../../src/events.js)).
766
- 6. Drag event objects on top of `_makeEvent`; `:drag-over`; the `dropAccept`
767
- normalisation table (§4.1); `text/uri-list` parsing (§4.2).
768
- 7. `XdndDrop` → `clipboard.read({ selection: 'XdndSelection', target })` with
769
- the drop timestamp → `onDrop` → `XdndFinished` with the watchdog.
770
- 8. `useDropTarget` in `src/components/`; types in `src/types/`; docs page;
771
- an `examples/` file-drop demo.
772
-
773
- ### Phase 2 — in-app drag source (no X11 at all)
774
-
775
- 9. `draggable`/`dragData`/`dragActions` props; threshold gesture over
776
- `capturePointer`; `onDragStart`/`onDrag`/`onDragEnd`.
777
- 10. `DragSession` local phase feeding the _same_ target-side dispatch, so
778
- Phase 1's handlers receive internal drags with `e.source === 'internal'`.
779
- 11. `dragPreview` → `<popup>` following the pointer; `:dragging`.
780
- 12. `useDragSource`.
781
-
782
- At this point reorderable lists, trees and kanban work with zero wire traffic,
783
- and the app-facing API is complete. Phase 3 changes no application code.
784
-
785
- ### Phase 3 — external drag source (XDND out)
786
-
787
- 13. Top-level cache: one `QueryTree` of root at drag start + geometry and
788
- `XdndAware`/`XdndProxy` per candidate; refresh on root
789
- `SubstructureNotify`. Per-motion target resolution is then local — the
790
- same design GTK uses, and the only way to avoid round trips per motion
791
- frame.
792
- 14. Promotion/demotion (§6.3): selection ownership, `XdndTypeList`,
793
- `GrabPointer` + action cursor, `XdndEnter`/`Position`/`Leave`/`Drop`.
794
- 15. `SelectionRequest` service for the offered types, `TARGETS` included,
795
- lazy `dragData` thunks resolved on demand; keep serving until
796
- `XdndFinished` (with the "throw out extremely old data" rule from the
797
- spec).
798
- 16. `XdndStatus` → cursor via `ChangeActivePointerGrab`; `XdndFinished` →
799
- `onDragEnd` with the performed action.
800
- 17. `XdndProxy` honouring; skip our own preview window in the descent.
801
-
802
- ### Phase 4 — ecosystem and polish
803
-
804
- 18. `XdndActionAsk` → `ContextMenu` from `XdndActionList` /
805
- `XdndActionDescription`. _Done, but not as sketched._ The two
806
- properties are read and handed to the app as `e.actions` /
807
- `e.actionDescriptions`, and `onDrop` settles the answer with
808
- `e.accept(action)`; the menu itself stays in application code. A
809
- built-in one would have to hold `XdndFinished` open for as long as it
810
- was on screen, and §9's first hazard is that a slow drop freezes the
811
- source application — so how long to make someone wait is the app's
812
- call, not the toolkit's. Reading the properties lazily, only when a
813
- position actually asks, keeps the two round trips off every other drag.
814
- 19. A `react-dnd` backend (separate package) over the seam from §5.
815
- 20. Auto-scroll on drag near a scroll container's edge — a default action in the
816
- router, the same shape as the existing wheel default
817
- ([events.js:294](../../src/events.js)). _Done._ It hangs off
818
- `_overAt`, so both transports get it from one implementation, and the
819
- step re-routes rather than re-measuring. The one thing the sketch did
820
- not anticipate: the step must **not** answer with an `XdndStatus`. A
821
- status is the reply to a position, and the source has sent none — so
822
- the fresh answer waits for the next real position instead.
823
-
824
- ---
825
-
826
- ## 8. Test plan
827
-
828
- Unusually strong here, and worth exploiting.
829
-
830
- - **Hermetic, two connections** — the `test/wm.test.js` pattern. node-x11's
831
- in-process JS server implements `SendEvent`, `SetSelectionOwner`,
832
- `ConvertSelection`, `GetSelectionOwner`, `ChangeProperty`, `GetProperty`,
833
- `QueryTree`, `TranslateCoordinates` and `GrabPointer`, and its
834
- `ConvertSelection` correctly turns into a `SelectionRequest` to the owner
835
- (`x11/lib/xserver/properties.js:186`). So one connection can play a **fake
836
- GTK source** (send `XdndEnter`/`Position`/`Drop`, own `XdndSelection`, serve
837
- `text/uri-list`) and assert react-x11's `XdndStatus`/`XdndFinished` byte by
838
- byte — and the mirror test can play a **fake target** against our source.
839
- Both halves of the protocol are testable with no `$DISPLAY`.
840
- - **Unit** — `text/uri-list` parsing (percent-encoding, CRLF, `#` comments,
841
- remote hosts), the type-alias table against the recorded type lists in §4.1,
842
- version negotiation, the `XdndFinished` watchdog.
843
- - **Node-tree** — registry lifecycle across mount/update/unmount/hide,
844
- enter/leave diffing, `:drag-over`, nested-`<window>` routing, and the
845
- zero-target suppression path (assert exactly one `XdndStatus` per
846
- window-entry).
847
- - **Manual, and be honest about the platform** — Linux/XWayland: Nautilus,
848
- Thunar, Dolphin, Firefox, Chromium, `mwh/dragon` both directions. macOS
849
- XQuartz: X-client to X-client only; **Finder → X11 does not work and is not
850
- our bug** ([XQuartz#173](https://github.com/XQuartz/XQuartz/issues/173)).
851
-
852
- ---
853
-
854
- ## 9. Hazards
855
-
856
- - **A slow `onDrop` freezes the source app.** The watchdog is not optional.
857
- - **Suppression rectangles are a footgun.** Freezing a zone that needed
858
- per-position feedback produces a dropzone that looks broken only on slow
859
- movements. Default off; `e.freeze()` opt-in; document the insertion-caret
860
- case explicitly.
861
- - **The source's window cache goes stale.** Windows move and stack during a
862
- drag. Refresh on `SubstructureNotify`, and re-descend on `ConfigureNotify`
863
- rather than trusting the cache for the whole gesture.
864
- - **Our own preview window is under the pointer.** Skip our ids in the
865
- descent (safe, because the grab makes us the only client searching).
866
- - **Timestamps.** Using `CurrentTime` for `ConvertSelection` mostly works and
867
- then fails against sources that validate — the worst failure profile there
868
- is. Thread `XdndDrop`'s `l[2]` through. _Done:_ `read({ time })` landed in
869
- ntk 5.4.0 and `_getData` passes the drop timestamp.
870
- - **Selection ownership outlives the drag.** ntk's `Clipboard` holds a
871
- selection until someone else takes it. An `XdndSelection` still owned after
872
- the drag ends is mostly harmless, but a stale offer answered after
873
- `XdndFinished` is the case the spec warns about ("throw out extremely old
874
- data"); release it explicitly at drag end. _Done:_ `_disown()` calls
875
- `clipboard.clear('XdndSelection')` (ntk 5.4.0), which releases with the
876
- acquisition timestamp — so a drag that already lost the selection to
877
- another client leaves it alone, where `CurrentTime` would have taken it
878
- back from them.
879
- - **Grabs versus `<popup>`.** Menus already take pointer grabs
880
- (`window.js:2017`). A drag beginning inside an open menu must not fight
881
- them; establish that the drag grab replaces the menu grab, and that closing
882
- the menu does not release ours.
883
-
884
- ---
885
-
886
- ## 10. Answering the three questions directly
887
-
888
- 1. **Is #126's API right?** The protocol plan is; the API needs three changes:
889
- ordinary host props instead of marker-carrying spread props (the existing
890
- dispatcher already does the work), declarative `dropAccept` promoted from a
891
- convenience to the mechanism that makes always-advertising safe, and no
892
- `dataTransfer` façade over an asynchronous transfer. Plus parsed `files`,
893
- a smarter `dropAccept` vocabulary, and the `XdndFinished` watchdog.
894
- 2. **Should the advertisement be lazy?** No — but the refcount you were
895
- reaching for is real; point it at the suppression rectangle instead of at
896
- the property. You get the same saving, with no race, no churn, and no
897
- Offscreen bookkeeping. And because `XdndAware` lives on top-levels only,
898
- none of this depends on the windowed/windowless decision from
899
- [windowed-regions.md](windowed-regions.md) — including a `<box windowed>`
900
- that document recommends never shipping.
901
- 3. **How well do the two sides work together?** Very well, if the drag session
902
- is one object with two transports rather than two features. Target-side
903
- routing and source-side target tracking are the same enter/leave/accept
904
- state machine; in-app drags reuse it with no X11 at all; and an app written
905
- against Phase 2 gains cross-application drops in Phase 3 without touching
906
- its code. With ntk 5.3.0 the source half is no longer gated on an upstream
907
- release ([§0.1](#01-audit-against-ntk-530)) — the only argument left for
908
- splitting it out of #126 is size, not dependency.
909
-
910
- ---
911
-
912
- ## 11. Sources
913
-
914
- - XDND protocol specification —
915
- [freedesktop mirror](https://www.accum.se/~vatten/XDND.html) (v5 field
916
- layouts, `XdndStatus` flag bits, `XdndFinished` v5 additions) and the
917
- [tkDND copy](https://users.iit.demokritos.gr/~petasis/Tcl/tkDND/XDND_Protocol/xdnd.html)
918
- (versions 0–4, property semantics, target non-response rules). Fields were
919
- cross-checked between the two; they agree.
920
- - [Drag-and-Drop Protocol for the X Window System](https://www.freedesktop.org/wiki/Specifications/XDND/)
921
- — canonical home of the spec.
922
- - [XQuartz#173 — drag and drop from Mac to X11 not working](https://github.com/XQuartz/XQuartz/issues/173)
923
- - [`dnd-core`](https://github.com/react-dnd/dnd-core) — the DOM-free core
924
- behind react-dnd's pluggable backends.
925
- - [`mwh/dragon`](https://github.com/mwh/dragon) — a small XDND source/target,
926
- useful as a manual test peer.
927
- - ntk releases audited in §0.1:
928
- [5.0.0](https://github.com/sidorares/ntk/releases/tag/v5.0.0) (buffered
929
- requests), [5.2.0](https://github.com/sidorares/ntk/releases/tag/v5.2.0)
930
- (`frameInFlight`),
931
- [5.3.0](https://github.com/sidorares/ntk/releases/tag/v5.3.0) —
932
- [ntk#164](https://github.com/sidorares/ntk/issues/164) (INCR write, any
933
- format), [ntk#163](https://github.com/sidorares/ntk/issues/163) (selection
934
- watch), [ntk#155](https://github.com/sidorares/ntk/issues/155) (cancellable
935
- `close`).
936
- - In-tree: [events.js](../../src/events.js), [nodes/window/droptarget.js](../../src/nodes/window/droptarget.js),
937
- [Reconciler.js](../../src/Reconciler.js), [styles.js](../../src/styles.js),
938
- [windowed-regions.md](windowed-regions.md), [ecosystem.md](../ecosystem.md);
939
- ntk 5.3.0 `lib/clipboard.js`, `lib/window.js`, `lib/cursor.js`,
940
- `lib/index.js`; node-x11 `lib/framebuffer.js`,
941
- `lib/xserver/properties.js`, `lib/xcore.js`.