react-x11 2.52.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -46,61 +46,36 @@ const { accent } = useSystemAppearance();
46
46
  <box style={{ borderColor: accent ?? '#2980b9' }} />;
47
47
  ```
48
48
 
49
- `accentText` is the ink the desktop puts on that fill, and it comes from the
50
- one source that has an opinion: AppKit writes white on every accent a Mac
51
- offers, including the orange and the pink a contrast ratio would put dark
52
- letters on, and every native control does. The portal names a fill and nothing
53
- about what goes on it, so there it is null and the palette picks the legible
54
- ink by contrast, as it does for any theme that names a fill and stops there.
55
-
56
- `selection` is the third colour a desktop names, and macOS is again the one
57
- that does: a selected menu row or list row is filled not with the accent but
58
- with `selectedContentBackgroundColor`, a darker cut of it — same hue,
59
- lightness 0.54 → 0.40 in dark and 0.45 in light for the orange, hand-tuned per
60
- accent rather than computed from it. The built-in palette uses it as
61
- `hoverBackground`; a menu lit in the raw accent beside a native one reads as
62
- too bright.
49
+ `accentText` is the ink the desktop puts on that fill. Only macOS names one —
50
+ white, on every accent it offers, as its native controls draw it. Elsewhere
51
+ it is null, and the palette picks the legible ink by contrast, as it does for
52
+ any theme that names a fill and stops there.
53
+
54
+ `selection` is the fill under a selected menu or list row, which macOS draws
55
+ not in the accent but in a darker cut of it (`selectedContentBackgroundColor`).
56
+ The built-in palette uses it as `hoverBackground`; a menu lit in the raw
57
+ accent beside a native one reads as too bright.
63
58
 
64
59
  ## The desktop's palette
65
60
 
66
- On macOS the rung reads the rest as well: the semantic colours AppKit paints
67
- its own windows and controls with, in the appearance the desktop is in. They
68
- arrive as `palette`, already in react-x11's vocabulary, and the built-in
69
- palette merges them over the scheme's own the way a `<ThemeProvider>` value
70
- merges — so an app that says nothing about colour comes up in the desktop's
71
- greys, inks and status colours, and follows every retune macOS ships.
72
-
73
- Two settings drive it, and they are not one colour. **Accent colour** is an
74
- index into a table of eight, each entry hand-tuned per appearance: the fill,
75
- the darker cut under a selected row, the focus ring and the pressed steps are
76
- all separate table entries, not a formula. **Highlight colour** is a free
77
- sRGB triple that only text selection and the caret read. Everything else is
78
- fixed per appearance.
79
-
80
- | token | AppKit source | how |
81
- | ------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------- |
82
- | `background` | `windowBackgroundColor` | as is: `#ececec` light, `#323232` dark |
83
- | `surface` | `controlBackgroundColor` | as is: white, `#1e1e1e` |
84
- | `surfaceHover` | the second `alternatingContentBackgroundColors` | composited over `surface` |
85
- | `text`, `textMuted` | `labelColor`, `secondaryLabelColor` | black or white at 85% and 50–55%, over the ground |
86
- | `border` | `separatorColor` | 10% ink over the ground |
87
- | `focusRing`, `borderFocus` | `keyboardFocusIndicatorColor` | the 50% ring over the ground |
88
- | `track` | `unemphasizedSelectedContentBackgroundColor` | as is |
89
- | `accent`, `accentText` | `controlAccentColor`, `alternateSelectedControlTextColor` | as is; the ink is white for all eight |
90
- | `accentHover`, `accentActive` | the accent with the pressed and deep-pressed effects | as is — darker in light, lighter in dark |
91
- | `hoverBackground`, `hoverText` | `selectedContentBackgroundColor`, the same ink | as is |
92
- | `selection` | `selectedTextBackgroundColor` | the Highlight colour; dark is each channel less 116/255 |
93
- | `caret` | `textInsertionPointColor` | the accent of the Highlight colour's name |
94
- | `link` | `linkColor` | as is, never the accent |
95
- | `danger`, `dangerHover` | `systemRedColor`, pressed | as is |
96
- | `success`, `warning`, `info` | `systemGreenColor`, `systemOrangeColor`, `systemBlueColor` | orange for warning: yellow fails as letters |
97
-
98
- AppKit's inks are translucent and every token here is a colour, so each is
99
- flattened over the window ground on the way in. The status inks and the
100
- pressed steps a table does not name come from `resolveTheme`, as for any
101
- theme. What this cannot see is desktop tinting: a dark window on macOS is
102
- `#323232` with a little of the wallpaper in it, and the wallpaper is not in
103
- any colour AppKit will hand out.
61
+ On macOS the rung also reads the semantic colours AppKit paints its own
62
+ windows and controls with, in the appearance the desktop is in. They arrive
63
+ as `palette`, already in react-x11's vocabulary, and the built-in palette
64
+ merges them over the scheme's own the way a `<ThemeProvider>` value merges —
65
+ so an app that says nothing about colour comes up in the desktop's greys,
66
+ inks and status colours, and follows every retune macOS ships.
67
+
68
+ The tokens it sets: `background`, `surface`, `surfaceHover`, `text`,
69
+ `textMuted`, `border`, `focusRing`, `borderFocus`, `track`, `accent`,
70
+ `accentText`, `accentHover`, `accentActive`, `hoverBackground`, `hoverText`,
71
+ `selection`, `caret`, `link`, `danger`, `dangerHover`, `success`, `warning`
72
+ and `info`. The accent family follows the user's **Accent colour**;
73
+ `selection` and `caret` follow the **Highlight colour**, a separate setting;
74
+ `link` is never the accent, and `warning` is orange, since yellow fails as
75
+ letters. Every token is an opaque colour, flattened over the window ground,
76
+ and the status inks and pressed steps it does not name come from
77
+ `resolveTheme`, as for any theme. Desktop tinting — the little of the wallpaper in a dark window — is
78
+ not in any colour AppKit hands out, so it is not reproduced.
104
79
 
105
80
  The portal names none of this, so on Linux the built-in palette stands and
106
81
  only the accent is taken.
@@ -113,11 +88,10 @@ visible flash.
113
88
 
114
89
  So **the answer from last time is on disk**, in
115
90
  `$XDG_CACHE_HOME/react-x11/appearance.json`, and it is read synchronously
116
- before the first render — 0.1 ms, no D-Bus, no await. None of these values
117
- changes more than a few times in a machine's life, so the remembered answer is
118
- almost always the right one; the ladder revalidates it in the background and
119
- replaces it if it moved. Stale-while-revalidate, where the stale value is
120
- nearly always the fresh one.
91
+ before the first render — 0.1 ms, no await. These values change a few times
92
+ in a machine's life, so the remembered answer is almost always the right
93
+ one; the ladder revalidates it in the background and replaces it if it
94
+ moved.
121
95
 
122
96
  `source` is how you tell them apart:
123
97
 
@@ -132,8 +106,7 @@ run 2 {colorScheme: 'dark', accent: '#ed5b00', source: 'cache'} ←
132
106
  ```
133
107
 
134
108
  Revalidation costs one re-render even when nothing moved, because `source`
135
- itself changed. That is deliberate: a `source` that stayed `'cache'` after the
136
- desktop had actually answered would be worth less than the render it saved.
109
+ itself changed.
137
110
 
138
111
  That leaves the very first launch on a machine, where there is nothing to
139
112
  remember. An app that must be exact rather than probably right waits:
@@ -146,12 +119,9 @@ root.render(<App />); // useSystemAppearance() is already correct
146
119
  ```
147
120
 
148
121
  `systemAppearance()` is the imperative twin of the hook, and never rejects.
149
-
150
- **Why it is not simply part of `createRoot()`.** Measured cold on a GNOME
151
- session: `createRoot()` alone 85 ms, `createRoot()` with a concurrent portal
152
- probe 124 ms. `dbus-native`'s import is CPU-bound, so it does not hide behind
153
- ntk's startup — and an app that never asks what colour the desktop is should
154
- not pay 40 ms to find out.
122
+ It is not part of `createRoot()` because the probe adds about 40 ms to a cold
123
+ start, which an app that never asks what colour the desktop is should not
124
+ pay.
155
125
 
156
126
  `REACT_X11_NO_APPEARANCE_CACHE=1` turns the file off for a process that must
157
127
  not touch the disk.
@@ -219,14 +189,12 @@ is dark, for a design whose two schemes are not one recolour of the other:
219
189
  <ThemeProvider value={{ background: '#fffdf7' }} dark={{ background: '#141210' }}>
220
190
  ```
221
191
 
222
- **The accent is followed on the same terms as the scheme.** The reason is
223
- the one that decided the scheme: an app that says nothing is asking to look
224
- like it belongs, and on a desktop that has an accent every control beside it is
225
- already that colour — on the Cocoa backend the app's own native checkboxes and
226
- default buttons are drawn by AppKit in the user's accent, so a `<Tabs>`
227
- indicator that stayed blue next to them looked like a bug. A brand keeps its
228
- own colour by naming it; a following provider's `value` wins over the desktop
229
- token for token, so name the family rather than the one colour:
192
+ **The accent is followed on the same terms as the scheme**: on a desktop
193
+ that has an accent, every control beside the app is already that colour — on
194
+ the Cocoa backend the app's own native checkboxes and default buttons are
195
+ drawn by AppKit in it. A following provider's `value` wins over the desktop
196
+ token for token, so a brand keeps its own colour by naming the family rather
197
+ than the one colour:
230
198
 
231
199
  ```jsx
232
200
  <ThemeProvider value={{ accent: '#e17055', accentHover: '#c0563a', hoverBackground: '#e17055' }}>
@@ -239,23 +207,10 @@ they say in any accent.
239
207
 
240
208
  ### Resizing
241
209
 
242
- The colour a window is _painted_ is only half of it. Dragging a window larger
243
- exposes area before the app can possibly have drawn it, and something has to
244
- be in that area meanwhile — so a window that never says what flashes a default
245
- on every drag of the corner, which on a dark palette is a bright rectangle.
246
-
247
- There are two copies of that colour, because a window here is double-buffered:
248
- the X **attribute** the server paints exposed area with, and the colour ntk
249
- clears the part of its backing store that a grow adds. react-x11 sets both —
250
- to the window's own `backgroundColor`, or the palette's — and keeps them in
251
- step when either moves. Nothing to do; it is worth knowing only because they
252
- are the pieces of the palette that live outside this renderer.
253
-
254
- Needs **ntk >= 6.6.1**, where `setBackgroundPixel` sets the pair together.
255
- Before that the backing store cleared to the screen's white whatever the
256
- window said, so enlarging a dark window flashed a white strip — and it
257
- survived, because a grow inside the pixmap's headroom reallocates nothing and
258
- so nothing damages it.
210
+ Dragging a window larger exposes area before the app can have drawn it.
211
+ react-x11 fills it with the window's own `backgroundColor`, or the
212
+ palette's, and keeps that in step when either moves, so a dark window does
213
+ not flash a bright rectangle on every drag of the corner. Nothing to do.
259
214
 
260
215
  ### Testing
261
216
 
@@ -302,49 +257,32 @@ new to learn. See [styling](styling.md) for what a transition covers.
302
257
 
303
258
  ## The ladder
304
259
 
305
- There is no cross-toolkit palette protocol. Nothing on a Linux desktop lets
306
- one toolkit ask another what colour a window background is; what exists is a
307
- shared theme _name_, and exactly four standardised appearance _values_. Those
308
- four are what this reads, from the best source the machine has:
309
-
310
- 1. **`org.freedesktop.portal.Settings`** — the real contract, and the only
311
- source with an accent colour. `ReadAll(['org.freedesktop.appearance'])`,
312
- live over `SettingChanged`. libadwaita, Qt 6.5+, Firefox and Electron all
313
- read this one.
314
- 2. **macOS** — `NSUserDefaults` and `NSWorkspace` through one long-lived
315
- `osascript` child, and only where the _process_ is running on a Mac. It
316
- comes before XSETTINGS on purpose: on a Mac, the Mac's own preference is
317
- the one the user set, and an XSETTINGS daemon there is something they
318
- installed by hand. It is in practice the only source on a Mac, on either
319
- backend — a stock XQuartz has no portal, no XSETTINGS manager and an
320
- unset `RESOURCE_MANAGER`. A _Linux_ process drawing to an XQuartz display
321
- never reaches this rung at all, which is correct: it cannot read that
322
- Mac's defaults.
323
- 3. **Windows** — answered by the win32 backend itself: the Personalize key
324
- for light or dark, DWM for the accent, `SystemParametersInfo` for contrast
325
- and reduced motion, live over `WM_SETTINGCHANGE` and its siblings. Before
326
- XSETTINGS for the same reason macOS is, and asked of the app rather than
327
- of the platform, so only a tree on the win32 backend reaches it.
328
- 4. **XSETTINGS** — pre-D-Bus, X11-only. `Net/ThemeName` is a _name_, so "is
329
- this dark" comes down to trusting the `-dark` suffix — which is exactly
330
- what the portal was invented to replace, so it is a fallback and never a
331
- correction. Live over `PropertyNotify`.
332
-
333
- `RESOURCE_MANAGER` is not a rung. It is where `Xft.dpi`, `Xft.rgba` and
334
- `Xcursor.*` live — font and cursor rendering — and there has never been an X
335
- resource for colour scheme, accent or contrast.
336
-
337
- **The first rung that answers owns all four values.** The rungs disagree:
338
- measured on one GNOME session, at one moment, the portal reported
339
- `reduced-motion: 0` while GNOME's own `enable-animations` was `false`. Taking
340
- the best-answered field from each would describe a desktop that does not
341
- exist.
342
-
343
- Nothing here holds the process open. The `SettingChanged` subscription leaves
344
- the match rule on the shared connection and releases its reference, so the
345
- socket goes back to `unref()`d: an app whose windows have closed still exits,
346
- and an app with a window on screen is awake anyway and gets the signal. The
347
- macOS child is spawned `unref()`d and killed on exit.
260
+ There is no cross-toolkit palette protocol: what a desktop exposes is a
261
+ shared theme _name_ and four standardised appearance _values_. Those four
262
+ are read from the best source the machine has:
263
+
264
+ 1. **`org.freedesktop.portal.Settings`** — the real contract on Linux, and
265
+ the only Linux source with an accent colour; live over `SettingChanged`.
266
+ libadwaita, Qt 6.5+, Firefox and Electron all read this one.
267
+ 2. **macOS** — only where the _process_ is running on a Mac, on either
268
+ backend; before XSETTINGS, because on a Mac the Mac's own preference is
269
+ the one the user set. A _Linux_ process drawing to an XQuartz display
270
+ never reaches this rung: it cannot read that Mac's settings.
271
+ 3. **Windows** — answered by the win32 backend itself: light or dark, the
272
+ accent, contrast and reduced motion, live. Before XSETTINGS for the same
273
+ reason as macOS, and only a tree on the win32 backend reaches it.
274
+ 4. **XSETTINGS** — X11 only. "Is this dark" comes down to a `-dark` suffix
275
+ on `Net/ThemeName`, so it is a fallback and never a correction. Live.
276
+
277
+ `RESOURCE_MANAGER` is not a rung: there has never been an X resource for
278
+ colour scheme, accent or contrast.
279
+
280
+ **The first rung that answers owns all four values.** The rungs can
281
+ disagree, and taking the best-answered field from each would describe a
282
+ desktop that does not exist.
283
+
284
+ Nothing here holds the process open: an app whose windows have closed still
285
+ exits.
348
286
 
349
287
  ### Not climbing it at all
350
288
 
@@ -371,46 +309,9 @@ off along with the other two integrations that talk to the session bus.
371
309
  | `contrast` | yes | yes | from the theme name |
372
310
  | `reducedMotion` | version 2 of the interface | yes | rarely — see below |
373
311
 
374
- `Gtk/EnableAnimations` is in GTK's key list but a settings daemon need not
375
- export it, and gnome-settings-daemon does not — 53 settings on the session
376
- this was written against, and it is not among them. So on GNOME the XSETTINGS
377
- rung reports no reduced motion rather than inventing an answer.
378
-
379
- ## Notes on the sources
380
-
381
- **1 is dark and 2 is light** in the portal's `color-scheme`. The ordering
382
- reads backwards, and getting it wrong inverts the appearance of every desktop
383
- that expressed a preference — which is precisely the set of machines that
384
- care.
385
-
386
- **The accent colour is a `(ddd)` of sRGB floats in [0, 1]**, and "unset" is
387
- spelled as values outside that range. It is converted to a `'#rrggbb'` string
388
- here, because every style in this renderer takes a CSS colour and
389
- `rgb(0.93, 0.36, 0)` is black.
390
-
391
- **The subscription goes on before the read.** A change landing between the
392
- read and the match rule is lost and nothing corrects it — the app stays stale
393
- for its whole lifetime. It is the same shape as the portal `Request` race in
394
- [the file dialog](filedialog.md).
395
-
396
- **On macOS the frameworks are read, not `defaults`.** Three of the four
397
- plausible `defaults` keys — `AppleHighlightColor`, `AppleAccentColor`,
398
- `com.apple.universalaccess increaseContrast` — do not exist until the user
399
- changes that setting, so "key not found" is the normal answer rather than the
400
- error case, and `AppleAccentColor` is an index into a table that has to be
401
- maintained by hand. `NSColor.controlAccentColor` is the colour itself, with
402
- Multicolor already resolved, `alternateSelectedControlTextColor` is the ink
403
- AppKit writes on it, `selectedContentBackgroundColor` is the shade under a
404
- selected row (both resolved in the appearance the desktop is in, since a
405
- dynamic colour in a bare `osascript` resolves as Aqua otherwise), and
406
- `NSWorkspace` answers the two accessibility flags directly.
407
-
408
- **A process reads the colours once.** `controlAccentColor` is resolved on
409
- first use and cached for the life of the process: after the user picks another
410
- accent, the same process still answers the old one, with or without an
411
- `NSApplication`. So the watcher prints its values once and, on the change
412
- notifications, exits — and react-x11 spawns another, whose first read is
413
- fresh. A watcher that re-read in place re-announced the same values, and a
414
- running app never saw the change. The Cocoa backend
415
- reads the same child: it is one source for both backends, and the accent it
416
- reports is the one AppKit draws the native bezels in.
312
+ On GNOME the XSETTINGS rung reports no reduced motion:
313
+ `Gtk/EnableAnimations` is in GTK's key list, but gnome-settings-daemon does
314
+ not export it.
315
+
316
+ Design notes — how each source is read, and the macOS colour mapping:
317
+ [design/appearance.md](../design/appearance.md).
@@ -54,60 +54,27 @@ when DevTools is absent, which is the usual case.
54
54
  `REACT_X11_EDITOR` picks the editor (`cursor` by default): `code`,
55
55
  `code-insiders`, `windsurf`, `vim`, `nvim`, or any other value, treated as a
56
56
  custom URI scheme. `react-x11 dev`'s error window opens a line the same way,
57
- through the same variable (`src/editor.js`, shared by both).
57
+ through the same variable.
58
58
 
59
59
  ## How it works
60
60
 
61
- `src/ClickToComponent.js`, installed from `Reconciler.js` when either
62
- `REACT_X11_CLICK_TO_COMPONENT` or `REACT_X11_EDITOR` is set, and
63
- `REACT_X11_CLICK_TO_COMPONENT` is not `0` — the way to turn it off under
64
- `react-x11 dev`, which sets it to `1` when it is unset. No
65
- babel/jsx-source plugin, no build-time instrumentation:
66
-
67
- - every host node already carries a reference to the fiber that created it
68
- (`node._reactFiber`, set in `createInstance`). It is this feature's own
69
- back-reference, not a DevTools one: `findFiberByHostInstance` is gone
70
- from react-reconciler 0.33, and click-to-component works with DevTools
71
- absent;
72
- - React 19 captures a real `Error` at every JSX call site in development
73
- mode (`fiber._debugStack`) and tracks who rendered what
74
- (`fiber._debugOwner`) — no `__source`/`__self` babel transform needed;
75
- - Node's `--enable-source-maps` (already on for both the plain `tsx`
76
- examples and `examples:tasks:hot`'s loader) has rewritten that Error's
77
- stack to original source before we ever see it, so resolving a location
78
- is just parsing stack-trace text and skipping the frames React and this
79
- renderer add on top of the call site (`react`, `react-reconciler`,
80
- `scheduler`, `react-x11`'s own `src/`) — no source-map library required
81
- (this is the one place
82
- the design diverges from a browser DOM version of the same idea: a
83
- browser doesn't remap `Error.stack` for you, so a tool like
84
- [show-component](https://github.com/sidorares/show-component) has to
85
- resolve source maps itself at runtime).
86
-
87
- Frames from _other_ packages are deliberately kept rather than skipped: a
88
- call site inside an installed component is a real location, it just isn't
89
- one in your source, and telling those apart is what lets the resolver climb
90
- the `_debugOwner` chain to the line that is. Skipping every `node_modules`
91
- frame instead walks past the library component entirely and lands on
92
- whichever frame of yours happens to be deeper in the render stack — usually
93
- the `render()` call at startup, which looks like an answer and isn't.
94
-
95
- Alt+Click is recognized in `EventManager._onMouseDown`
96
- (`src/events.js`) — `native.buttons & MOD.Alt` is X11's `Mod1Mask` bit, the
97
- same `buttons` bitmask every synthetic event's `altKey` reads — ahead of the
98
- normal press handling, so it never also starts a drag or moves focus.
99
-
100
- GUI editors (cursor/code/code-insiders/windsurf, or any unrecognized
101
- `REACT_X11_EDITOR` value treated as a raw scheme) are opened by navigating
102
- their registered URI scheme — `open cursor://file/abs/path:line:col` on
103
- macOS, `xdg-open` elsewhere — the same idea show-component uses in a
104
- browser via `window.location.href`, just handed to the OS instead. This is
105
- noticeably faster than spawning the editor's CLI shim directly: a CLI like
106
- `cursor -g ...` is a wrapper script that re-detects and re-execs the real
107
- app on every call, while the URI scheme is delivered straight to the
108
- already-running instance by Launch Services / xdg-open. vim and nvim have
109
- no URI scheme to navigate, so those are still spawned directly with a
110
- `+call cursor(line, column)` argument.
61
+ It is installed when `REACT_X11_CLICK_TO_COMPONENT` or `REACT_X11_EDITOR` is
62
+ set, unless `REACT_X11_CLICK_TO_COMPONENT=0` — the way to turn it off under
63
+ `react-x11 dev`, which sets it to `1` when it is unset. No babel/jsx-source
64
+ plugin and no build step: React 19's development-mode call-site stacks and
65
+ owner chain are all it reads.
66
+
67
+ - **Locations are as good as the stack.** It reads the source location off
68
+ `Error.stack`, so run with Node's `--enable-source-maps` (on in the `tsx`
69
+ examples) or locations point at compiled output.
70
+ - **Alt+Click is taken before normal press handling**, so it never also
71
+ starts a drag or moves focus.
72
+ - **GUI editors are opened through their URI scheme** —
73
+ `open cursor://file/abs/path:line:col` on macOS, `xdg-open` elsewhere — which
74
+ reaches the already-running instance; vim and nvim are spawned with a
75
+ `+call cursor(line, column)` argument.
76
+
77
+ Design notes: [design/click-to-component.md](../design/click-to-component.md#how-it-works).
111
78
 
112
79
  ## Caveats
113
80
 
package/docs/clipboard.md CHANGED
@@ -20,20 +20,17 @@ middle-click paste, select-to-own. This page is for everything else: a
20
20
  canvas that copies an image, a list that pastes files, a document view with
21
21
  its own Copy item.
22
22
 
23
- > **On the Cocoa backend** the clipboard is `NSPasteboard.general`, and the
24
- > difference from X11 is structural rather than cosmetic: X's model is
25
- > _ownership of a selection_, negotiated per paste, and macOS's is a
26
- > _pasteboard the system holds_. Everything below works there — writing,
23
+ > **On the Cocoa backend** the clipboard is `NSPasteboard.general`: a
24
+ > pasteboard the system holds, rather than X's _ownership of a selection_
25
+ > negotiated per paste. Everything below works there — writing,
27
26
  > [reading](#reading) any type the pasteboard carries, `targets()` and
28
27
  > [`watch()`](#knowing-when-it-changes) — with three differences, each
29
- > described where it applies: [`PRIMARY`](#primary-is-x11-only) is X11-only
30
- > rather than missing, since nothing on macOS is filled by selecting text;
28
+ > described where it applies: [`PRIMARY`](#primary-is-x11-only) is X11-only;
31
29
  > `targets()` names the pasteboard's types rather than an owner's atoms; and
32
- > `watch()` asks the pasteboard's change count while something is watching,
33
- > since nothing announces a copy. A [multi-flavour payload](#writing) is
34
- > written whole when the bridge has `pasteboardWrite` (windowkit/appkit#119);
35
- > an older bridge keeps the text and says once, in development, what it
36
- > dropped.
30
+ > `watch()` polls the pasteboard's change count. A
31
+ > [multi-flavour payload](#writing) is written whole when the bridge has
32
+ > `pasteboardWrite`; an older bridge keeps the text and says once, in
33
+ > development, what it dropped.
37
34
 
38
35
  ## Two clipboards
39
36
 
@@ -53,9 +50,8 @@ which is what the built-in text controls do.
53
50
  Two consequences worth knowing, because neither matches a browser:
54
51
 
55
52
  - **The data lives in this process.** Nothing is stored by the server, so
56
- when the app exits the clipboard goes with it — normal X behaviour, and
57
- the reason desktops run clipboard managers. Nothing negotiates with one
58
- yet.
53
+ when the app exits the clipboard goes with it (normal X behaviour; there is
54
+ no clipboard-manager handoff).
59
55
  - **Someone else copying takes it away.** Ownership is exclusive; there is
60
56
  no "clipboard history" to fall back on.
61
57
 
@@ -71,22 +67,15 @@ your own — is one nobody can paste from:
71
67
  X's answers for a selection nobody owns;
72
68
  - `clear()` leaves the pasteboard alone, and `watch()` never fires.
73
69
 
74
- The **win32** backend answers every selection but `CLIPBOARD` the same way,
75
- since Windows has no equivalent either (and writes only text there so far).
76
- The **Wayland** backend is the other X-like one: it has `PRIMARY` too, over
77
- the primary-selection protocol where the compositor offers it
78
- (`src/wayland/clipboard.js`), so "X11-only" here means "not on macOS or
79
- Windows".
80
-
81
- That is what keeps selecting from being copying. A drag across a
82
- `selectable` surface, a select-all, shift+arrows in a field: each takes
83
- `PRIMARY` on X11, and on macOS none of them replaces what the user copied
84
- somewhere else. Code written to the X11 convention needs no platform check.
85
-
86
- `PRIMARY` is deliberately not kept inside the app instead. Its point is the
87
- paste in _another_ application; all an in-process copy could keep is a
88
- middle click that types into a text field, which no Mac text field does. An
89
- app that wants select-to-copy on macOS anyway — a terminal emulator, say —
70
+ The **win32** backend answers every selection but `CLIPBOARD` the same way
71
+ (and writes only text there so far). The **Wayland** backend has `PRIMARY`
72
+ too, over the primary-selection protocol where the compositor offers it — so
73
+ "X11-only" here means "not on macOS or Windows".
74
+
75
+ So selecting is never copying there: a drag across a `selectable` surface, a
76
+ select-all or shift+arrows takes `PRIMARY` on X11 and leaves the macOS
77
+ pasteboard alone. Code written to the X11 convention needs no platform check.
78
+ An app that wants select-to-copy on macOS anyway — a terminal emulator, say —
90
79
  can write `CLIPBOARD` itself: a surface's `selectedText()` is the text, and
91
80
  `onSelectionChange` says when it moved.
92
81
 
@@ -129,9 +118,8 @@ text-ish types to a string. It answers `null` when the owner has nothing of
129
118
  that kind, because "is there an image on the clipboard?" is a question, not
130
119
  an error.
131
120
 
132
- The groups exist because the same payload arrives under different names
133
- depending on who copied it, and exact-match filtering silently misses most
134
- of the desktop:
121
+ The same payload arrives under different names depending on who copied it,
122
+ so a group matches all of them:
135
123
 
136
124
  | group | matches |
137
125
  | --------- | ---------------------------------------------------------------------------------------------------------- |
@@ -139,28 +127,21 @@ of the desktop:
139
127
  | `'files'` | `text/uri-list`, `application/x-kde4-urilist`, `x-special/gnome-copied-files` |
140
128
  | `'uris'` | `text/uri-list`, `text/x-moz-url`, `_NETSCAPE_URL` |
141
129
 
142
- This is the same table [drag and drop](drag-and-drop.md) uses, and
143
- deliberately so: the flavours a file manager offers on a drop are the ones
144
- it offers on a copy. Groups do not overlap — a Firefox link is
130
+ This is the same table [drag and drop](drag-and-drop.md) uses. Groups do not
131
+ overlap — a Firefox link is
145
132
  `text/x-moz-url`, which is in `uris`, not `text`.
146
133
 
147
134
  On the Cocoa backend `targets()` lists the pasteboard's types in these
148
135
  names: a type with a MIME type under that (`public.png` is `image/png`,
149
136
  Finder's `public.file-url` is `text/uri-list`), text under both its X and
150
- MIME names, and any other type under its own identifier. That last rule is
151
- what lets an app honour a password manager: one that copies a secret puts
152
- [`org.nspasteboard.ConcealedType`](http://nspasteboard.org) beside it, the
153
- macOS convention for "keep this out of clipboard history", and a clipboard
154
- history should skip any copy whose `targets()` include it (or
155
- `org.nspasteboard.TransientType`). Files copied in Finder are one item each,
156
- and `read('files')` reads them as one uri-list.
157
-
158
- Text is read through the bridge's own text verb. Anything else is read from
159
- the pasteboard's items: through `pasteboardItems`/`pasteboardItemData` on a
160
- bridge that has them, and otherwise through `osascript -l JavaScript`, one
161
- short-lived child of about 60 ms per `targets()` or `read()`. The type list
162
- is kept against the pasteboard's change count, so asking again with nothing
163
- copied in between costs nothing.
137
+ MIME names, and any other type under its own identifier. So an app can honour
138
+ a password manager: a clipboard history should skip any copy whose
139
+ `targets()` include [`org.nspasteboard.ConcealedType`](http://nspasteboard.org)
140
+ or `org.nspasteboard.TransientType`. Files copied in Finder are one item each,
141
+ and `read('files')` reads them as one uri-list. Reading anything but text
142
+ costs an `osascript` child (about 60 ms per `targets()` or `read()`) on a
143
+ bridge without `pasteboardItems`; asking again with nothing copied in between
144
+ costs nothing.
164
145
 
165
146
  `readFiles()` is `read('files')` with the RFC 2483 parsing applied: CRLF
166
147
  separators, `#` comment lines, percent-decoding. `path` is present only for
@@ -195,15 +176,13 @@ something changes. `owner === 0` means nothing is on the clipboard, which is
195
176
  the case a Paste menu item wants. It rejects on a server without XFixes;
196
177
  every X server since about 2004 has it.
197
178
 
198
- On the Cocoa backend nothing announces a copy, so `watch()` is a poll after
199
- all: it asks the pasteboard's change count, which every write bumps, every
200
- 250 ms while anything is watching, and stops with the last unsubscribe. The
201
- count is a published number on the bridge, under a microsecond to read, and
202
- a change costs one look at what the pasteboard now holds, to say whether
203
- `owner` is `0`. No window owns the pasteboard, so `owner` is `-1` when it
204
- holds anything; `ev.changeCount` is the pasteboard's own count. Two copies
205
- inside one poll are one change, and the app's own copies are changes too, as
206
- they are on X11. It rejects on a bridge without `pasteboardChangeCount`.
179
+ On the Cocoa backend nothing announces a copy, so `watch()` polls the
180
+ pasteboard's change count every 250 ms while anything is watching (cheap; it
181
+ stops with the last unsubscribe). No window owns the pasteboard, so `owner` is
182
+ `-1` when it holds anything; `ev.changeCount` is the pasteboard's own count.
183
+ Two copies inside one poll are one change, and the app's own copies are
184
+ changes too, as they are on X11. It rejects on a bridge without
185
+ `pasteboardChangeCount`.
207
186
 
208
187
  ## Timestamps
209
188
 
@@ -279,3 +258,5 @@ raw protocol. See ntk's own clipboard documentation for the ICCCM details.
279
258
  a bare `writeText()` offers both.
280
259
  - **No image decoding.** `read('image/png')` hands back the bytes; turning
281
260
  them into something drawable is yours.
261
+
262
+ Design notes: [design/clipboard.md](../design/clipboard.md).