react-x11 2.51.1 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# System appearance
|
|
2
|
+
|
|
3
|
+
What the desktop looks like — light or dark, the accent colour, contrast, and
|
|
4
|
+
whether the user asked for less motion — as four values an app renders from.
|
|
5
|
+
|
|
6
|
+
```jsx
|
|
7
|
+
import { useSystemAppearance } from 'react-x11';
|
|
8
|
+
|
|
9
|
+
const { colorScheme, accent, contrast, reducedMotion } = useSystemAppearance();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Most apps never need it.** react-x11's built-in palette already follows the
|
|
13
|
+
desktop, so an app that says nothing about colour is dark on a dark desktop
|
|
14
|
+
and light on a light one, and its buttons, tabs and menu highlights are the
|
|
15
|
+
desktop's accent where the desktop has one — see
|
|
16
|
+
[following the desktop](#following-the-desktop) below. Reach for the hook when
|
|
17
|
+
you want the values themselves: reduced motion, the accent as a colour for
|
|
18
|
+
something that is not a widget, or a design decision that is not a palette.
|
|
19
|
+
|
|
20
|
+
## The four values
|
|
21
|
+
|
|
22
|
+
| | |
|
|
23
|
+
| --------------- | ---------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `colorScheme` | `'light'`, `'dark'` or `'no-preference'` |
|
|
25
|
+
| `accent` | `'#ed5b00'`, or **null** |
|
|
26
|
+
| `accentText` | the ink the desktop writes on its accent — `'#ffffff'` on macOS — or **null** |
|
|
27
|
+
| `selection` | the fill under a selected menu or list row — a darker cut of the accent on macOS — or **null** |
|
|
28
|
+
| `palette` | the desktop's whole palette as tokens, on macOS — or **null** |
|
|
29
|
+
| `contrast` | `'normal'` or `'high'` |
|
|
30
|
+
| `reducedMotion` | `true` when the user asked for less animation |
|
|
31
|
+
| `source` | which rung answered — `'portal'`, `'xsettings'`, `'macos'`, `'windows'`, `'cache'`, or null |
|
|
32
|
+
|
|
33
|
+
Two of these are easy to get wrong.
|
|
34
|
+
|
|
35
|
+
**`'no-preference'` means _use your own default_**, not "use light". It is
|
|
36
|
+
what a desktop says when it has no opinion, and an app whose own design is
|
|
37
|
+
dark should stay dark there.
|
|
38
|
+
|
|
39
|
+
**`accent` is null far more often than it is not.** Most portal backends
|
|
40
|
+
implement `color-scheme` and nothing else, and XSETTINGS has no accent key at
|
|
41
|
+
all. Fall back to your own brand colour rather than to grey — an app that goes
|
|
42
|
+
colourless on a desktop that simply did not answer the question looks broken.
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
const { accent } = useSystemAppearance();
|
|
46
|
+
<box style={{ borderColor: accent ?? '#2980b9' }} />;
|
|
47
|
+
```
|
|
48
|
+
|
|
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.
|
|
58
|
+
|
|
59
|
+
## The desktop's palette
|
|
60
|
+
|
|
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.
|
|
79
|
+
|
|
80
|
+
The portal names none of this, so on Linux the built-in palette stands and
|
|
81
|
+
only the accent is taken.
|
|
82
|
+
|
|
83
|
+
## The first frame
|
|
84
|
+
|
|
85
|
+
None of this can be known synchronously — the answer is a D-Bus call away —
|
|
86
|
+
and an app that renders light and then switches to dark a moment later has a
|
|
87
|
+
visible flash.
|
|
88
|
+
|
|
89
|
+
So **the answer from last time is on disk**, in
|
|
90
|
+
`$XDG_CACHE_HOME/react-x11/appearance.json`, and it is read synchronously
|
|
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.
|
|
95
|
+
|
|
96
|
+
`source` is how you tell them apart:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
run 1 {colorScheme: 'no-preference', accent: null, source: null} ← nothing remembered
|
|
100
|
+
{colorScheme: 'dark', accent: '#ed5b00', source: 'portal'} ← 78 ms later
|
|
101
|
+
|
|
102
|
+
run 2 {colorScheme: 'dark', accent: '#ed5b00', source: 'cache'} ← 0.1 ms, first render
|
|
103
|
+
{colorScheme: 'dark', accent: '#ed5b00', source: 'portal'} ← revalidated
|
|
104
|
+
|
|
105
|
+
(`accentText`, `selection`, `contrast` and `reducedMotion` travel with them.)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Revalidation costs one re-render even when nothing moved, because `source`
|
|
109
|
+
itself changed.
|
|
110
|
+
|
|
111
|
+
That leaves the very first launch on a machine, where there is nothing to
|
|
112
|
+
remember. An app that must be exact rather than probably right waits:
|
|
113
|
+
|
|
114
|
+
```jsx
|
|
115
|
+
import { createRoot, systemAppearance } from 'react-x11';
|
|
116
|
+
|
|
117
|
+
const [root] = await Promise.all([createRoot(), systemAppearance()]);
|
|
118
|
+
root.render(<App />); // useSystemAppearance() is already correct
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`systemAppearance()` is the imperative twin of the hook, and never rejects.
|
|
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.
|
|
125
|
+
|
|
126
|
+
`REACT_X11_NO_APPEARANCE_CACHE=1` turns the file off for a process that must
|
|
127
|
+
not touch the disk.
|
|
128
|
+
|
|
129
|
+
## Following the desktop
|
|
130
|
+
|
|
131
|
+
**Nothing.** That is the whole section.
|
|
132
|
+
|
|
133
|
+
react-x11's built-in palette _is_ the desktop's: an app that says nothing
|
|
134
|
+
about colour is dark on a dark desktop and light on a light one, the way a
|
|
135
|
+
GTK or Qt app is, and where the desktop reports an accent — macOS always,
|
|
136
|
+
GNOME 47+ and KDE through the portal — `$accent` and the family around it
|
|
137
|
+
(`accentHover`, the pressed step, `hoverBackground`, the focus ring) are that
|
|
138
|
+
colour, and `hoverBackground` is the desktop's own selection shade where it
|
|
139
|
+
names one. On macOS the whole palette is the desktop's — see
|
|
140
|
+
[the desktop's palette](#the-desktops-palette). The window background, the widgets and every `$token` in a style all
|
|
141
|
+
come from it.
|
|
142
|
+
|
|
143
|
+
```jsx
|
|
144
|
+
// this app follows the desktop
|
|
145
|
+
function App() {
|
|
146
|
+
return (
|
|
147
|
+
<window width={400} height={200}>
|
|
148
|
+
<box style={{ flexGrow: 1, padding: 16, backgroundColor: '$background' }}>
|
|
149
|
+
<text style={{ color: '$text' }}>Hello</text>
|
|
150
|
+
<Button primary label="Save" />
|
|
151
|
+
</box>
|
|
152
|
+
</window>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A `<window>` with no `backgroundColor` takes the palette's, so even that line
|
|
158
|
+
is optional.
|
|
159
|
+
|
|
160
|
+
### Overriding
|
|
161
|
+
|
|
162
|
+
`<ThemeProvider value={…}>` layers your palette **over the scheme in force**,
|
|
163
|
+
so it names what your app changes and everything else keeps following:
|
|
164
|
+
|
|
165
|
+
```jsx
|
|
166
|
+
<ThemeProvider value={{ accent: '#e17055', radius: 10 }}>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
That app has its own accent and corner radius on both a light and a dark
|
|
170
|
+
desktop, and never wrote a second palette.
|
|
171
|
+
|
|
172
|
+
`colorScheme` pins a subtree, for a design that only works one way or a
|
|
173
|
+
preference the app owns rather than the desktop:
|
|
174
|
+
|
|
175
|
+
```jsx
|
|
176
|
+
<ThemeProvider value={brand} colorScheme="light"> // never follows
|
|
177
|
+
<ThemeProvider value={brand} colorScheme={settings.theme}> // the app decides
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Pinning is also the complete opt-out.** A pinned provider subscribes to
|
|
181
|
+
nothing and the widgets under it read the palette it published, so an app that
|
|
182
|
+
says `colorScheme="light"` once at the top never has react-x11 ask the desktop
|
|
183
|
+
anything.
|
|
184
|
+
|
|
185
|
+
`dark` is the other half — a palette layered on only when the scheme in force
|
|
186
|
+
is dark, for a design whose two schemes are not one recolour of the other:
|
|
187
|
+
|
|
188
|
+
```jsx
|
|
189
|
+
<ThemeProvider value={{ background: '#fffdf7' }} dark={{ background: '#141210' }}>
|
|
190
|
+
```
|
|
191
|
+
|
|
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:
|
|
198
|
+
|
|
199
|
+
```jsx
|
|
200
|
+
<ThemeProvider value={{ accent: '#e17055', accentHover: '#c0563a', hoverBackground: '#e17055' }}>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
And a pinned `colorScheme` follows nothing, the accent included. What does
|
|
204
|
+
**not** move with the accent is `info`: a note is blue everywhere, under a
|
|
205
|
+
green accent as under an orange one, and `success`/`warning`/`danger` say what
|
|
206
|
+
they say in any accent.
|
|
207
|
+
|
|
208
|
+
### Resizing
|
|
209
|
+
|
|
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.
|
|
214
|
+
|
|
215
|
+
### Testing
|
|
216
|
+
|
|
217
|
+
`react-x11/test` **pins the colour scheme to light**, and that is not a
|
|
218
|
+
convenience: with a palette that follows the desktop, an unpinned suite renders
|
|
219
|
+
in whatever colours the machine running it happens to be in, so a pixel
|
|
220
|
+
assertion would be green on a light desktop and red on a dark one with nothing
|
|
221
|
+
in the test to say so.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
await renderX11(<App />, { fonts: FONTS }); // light
|
|
225
|
+
await renderX11(<App />, { fonts: FONTS, colorScheme: 'dark' }); // dark
|
|
226
|
+
await renderX11(<App />, { fonts: FONTS, colorScheme: 'system' }); // the real one
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`createMockApp()` pins the same way. `'system'` is for a test that is _about_
|
|
230
|
+
what the desktop reports.
|
|
231
|
+
|
|
232
|
+
The other values pin too, from the first render or under a mounted tree —
|
|
233
|
+
which is how a component that follows `reducedMotion`, `contrast` or the
|
|
234
|
+
accent is tested, change included. A pinned appearance reports
|
|
235
|
+
`source: 'test'`.
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
await renderX11(<App />, { appearance: { reducedMotion: true } });
|
|
239
|
+
await setAppearance({ reducedMotion: false }); // re-renders, repaints
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
See [testing](testing.md#the-desktops-appearance) for the rules.
|
|
243
|
+
|
|
244
|
+
## Reduced motion
|
|
245
|
+
|
|
246
|
+
`reducedMotion` is reported, not applied. Nothing in the renderer switches
|
|
247
|
+
animation off behind your back — an app with `transition: 150` keeps it until
|
|
248
|
+
the app decides otherwise:
|
|
249
|
+
|
|
250
|
+
```jsx
|
|
251
|
+
const { reducedMotion } = useSystemAppearance();
|
|
252
|
+
<box style={{ transition: reducedMotion ? 0 : 150, backgroundColor: fill }} />;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`transition: 0` is already how "no animation" is spelled, so there is nothing
|
|
256
|
+
new to learn. See [styling](styling.md) for what a transition covers.
|
|
257
|
+
|
|
258
|
+
## The ladder
|
|
259
|
+
|
|
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.
|
|
286
|
+
|
|
287
|
+
### Not climbing it at all
|
|
288
|
+
|
|
289
|
+
```jsx
|
|
290
|
+
await createRoot({ desktop: { appearance: false } });
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
For an app that owns its palette, an embedder that follows the desktop its own
|
|
294
|
+
way, and a test that needs the same answer on every machine. The ladder does
|
|
295
|
+
not run and **the remembered answer is not read either**: `colorScheme` stays
|
|
296
|
+
`'no-preference'` with `source: null`, which means use your own default. See
|
|
297
|
+
[desktop.md](desktop.md#turning-the-desktop-off) — `desktop: false` turns this
|
|
298
|
+
off along with the other two integrations that talk to the session bus.
|
|
299
|
+
|
|
300
|
+
## What each rung can actually answer
|
|
301
|
+
|
|
302
|
+
| | portal | macOS | XSETTINGS |
|
|
303
|
+
| --------------- | ------------------------------------ | ----- | ------------------- |
|
|
304
|
+
| `colorScheme` | yes | yes | from the theme name |
|
|
305
|
+
| `accent` | yes, where the backend implements it | yes | **no such key** |
|
|
306
|
+
| `accentText` | no — the palette picks by contrast | yes | no |
|
|
307
|
+
| `selection` | no — the accent itself | yes | no |
|
|
308
|
+
| `palette` | no | yes | no |
|
|
309
|
+
| `contrast` | yes | yes | from the theme name |
|
|
310
|
+
| `reducedMotion` | version 2 of the interface | yes | rarely — see below |
|
|
311
|
+
|
|
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).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Click to component
|
|
2
|
+
|
|
3
|
+
Alt+Click any rendered element to jump straight to the JSX line that created
|
|
4
|
+
it, in your editor. On macOS that's **Option+Click**, on both backends: the
|
|
5
|
+
Cocoa backend maps ⌥ to Mod1 itself, and XQuartz maps it there by default
|
|
6
|
+
("Option keys send Alt_L and Alt_R" in its Input preferences). The
|
|
7
|
+
installed-feature log line says whichever one applies to your platform.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Under `react-x11 dev` it is already on. Elsewhere:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
REACT_X11_EDITOR=code npm run examples:tasks # naming an editor enables it
|
|
15
|
+
# or, to use the default (cursor):
|
|
16
|
+
REACT_X11_CLICK_TO_COMPONENT=1 npm run examples:tasks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Alt/Option+Click a task row, a button, anything — its owning component's source
|
|
20
|
+
opens at the exact line, the clicked element flashes blue for a moment, and
|
|
21
|
+
the console prints what was resolved:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
[click-to-component] TaskRow → examples/tasks.jsx:42:7
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Alt+Shift+Click also logs the full ownership chain (who rendered who, up to
|
|
28
|
+
the root) to the console — handy when the immediate owner is a shared
|
|
29
|
+
wrapper rather than the component you actually meant.
|
|
30
|
+
|
|
31
|
+
## Elements you didn't write
|
|
32
|
+
|
|
33
|
+
An element rendered by an installed component — a design system's
|
|
34
|
+
`<Toolbar>`, a chart library's internals — has its JSX call site inside that
|
|
35
|
+
package, not in your code. Clicking one resolves to the nearest owner up the
|
|
36
|
+
chain that _is_ your source: the `<Toolbar ... />` line in your file that put
|
|
37
|
+
it on screen. The console line says how far it had to climb, so a location
|
|
38
|
+
that doesn't match what you clicked is never a surprise:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
[click-to-component] App → src/ui/App.tsx:432:7 (1 owner up from the clicked <box>)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
When no owner in the chain is application source (a screen rendered entirely
|
|
45
|
+
by a package), the package's own file is opened instead and the line is
|
|
46
|
+
marked `— inside an installed package`. Only a tree with no React debug info
|
|
47
|
+
at all resolves to nothing.
|
|
48
|
+
|
|
49
|
+
With React DevTools attached as well (`REACT_X11_DEVTOOLS=1`, see
|
|
50
|
+
[devtools.md](devtools.md)), the same click also selects that element in the
|
|
51
|
+
DevTools tree — one click to the source _and_ to its props. Nothing changes
|
|
52
|
+
when DevTools is absent, which is the usual case.
|
|
53
|
+
|
|
54
|
+
`REACT_X11_EDITOR` picks the editor (`cursor` by default): `code`,
|
|
55
|
+
`code-insiders`, `windsurf`, `vim`, `nvim`, or any other value, treated as a
|
|
56
|
+
custom URI scheme. `react-x11 dev`'s error window opens a line the same way,
|
|
57
|
+
through the same variable.
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
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).
|
|
78
|
+
|
|
79
|
+
## Caveats
|
|
80
|
+
|
|
81
|
+
- Needs React running in development mode (fiber debug fields are stripped
|
|
82
|
+
in production builds).
|
|
83
|
+
- The editor is launched detached, so a failure is only visible as a
|
|
84
|
+
warning: an editor whose URI scheme nothing claims (`REACT_X11_EDITOR`
|
|
85
|
+
naming an editor that isn't installed) prints
|
|
86
|
+
`click-to-component — \`open cursor://...\` exited 1` next to the
|
|
87
|
+
resolved location rather than silently doing nothing.
|
|
88
|
+
- A node's `_reactFiber` is only refreshed on mount, not on every update —
|
|
89
|
+
usually harmless (the JSX call site of a given element rarely moves
|
|
90
|
+
between renders of the same tree shape), but a node that was
|
|
91
|
+
conditionally re-typed can point at a stale-but-structurally-similar
|
|
92
|
+
fiber.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Clipboard
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
import { useClipboard } from 'react-x11';
|
|
5
|
+
|
|
6
|
+
function Editor({ value }) {
|
|
7
|
+
const clipboard = useClipboard();
|
|
8
|
+
return (
|
|
9
|
+
<box
|
|
10
|
+
onKeyDown={(e) => {
|
|
11
|
+
if (e.ctrlKey && e.key === 'c') clipboard.writeText(value);
|
|
12
|
+
}}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`<textinput>` and `<textarea>` already do all of this for you — Ctrl+C/X/V,
|
|
19
|
+
middle-click paste, select-to-own. This page is for everything else: a
|
|
20
|
+
canvas that copies an image, a list that pastes files, a document view with
|
|
21
|
+
its own Copy item.
|
|
22
|
+
|
|
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,
|
|
26
|
+
> [reading](#reading) any type the pasteboard carries, `targets()` and
|
|
27
|
+
> [`watch()`](#knowing-when-it-changes) — with three differences, each
|
|
28
|
+
> described where it applies: [`PRIMARY`](#primary-is-x11-only) is X11-only;
|
|
29
|
+
> `targets()` names the pasteboard's types rather than an owner's atoms; and
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Two clipboards
|
|
36
|
+
|
|
37
|
+
X has no clipboard buffer. "Copy" means owning a **selection** — a named
|
|
38
|
+
thing the server tracks — and answering conversion requests from whoever
|
|
39
|
+
pastes. There are two in everyday use, and they are independent:
|
|
40
|
+
|
|
41
|
+
| selection | filled by | pasted with |
|
|
42
|
+
| ----------- | ------------------------------------------ | -------------------- |
|
|
43
|
+
| `CLIPBOARD` | an explicit Copy or Cut | Ctrl+V, an Edit menu |
|
|
44
|
+
| `PRIMARY` | _selecting text at all_, with no Copy step | middle click |
|
|
45
|
+
|
|
46
|
+
Every call below takes `{ selection }` and defaults to `CLIPBOARD`. The
|
|
47
|
+
X11-native behaviour is to write `PRIMARY` whenever your selection changes,
|
|
48
|
+
which is what the built-in text controls do.
|
|
49
|
+
|
|
50
|
+
Two consequences worth knowing, because neither matches a browser:
|
|
51
|
+
|
|
52
|
+
- **The data lives in this process.** Nothing is stored by the server, so
|
|
53
|
+
when the app exits the clipboard goes with it (normal X behaviour; there is
|
|
54
|
+
no clipboard-manager handoff).
|
|
55
|
+
- **Someone else copying takes it away.** Ownership is exclusive; there is
|
|
56
|
+
no "clipboard history" to fall back on.
|
|
57
|
+
|
|
58
|
+
### `PRIMARY` is X11-only
|
|
59
|
+
|
|
60
|
+
macOS has one pasteboard, and it is `CLIPBOARD`'s: nothing there is filled
|
|
61
|
+
by selecting text, and a middle click pastes nothing. So on the Cocoa
|
|
62
|
+
backend every selection but `CLIPBOARD` — `PRIMARY`, `SECONDARY`, a name of
|
|
63
|
+
your own — is one nobody can paste from:
|
|
64
|
+
|
|
65
|
+
- `write()` and `writeText()` resolve and leave the pasteboard alone;
|
|
66
|
+
- `read()` answers `null`, `readText()` rejects and `targets()` is `[]` —
|
|
67
|
+
X's answers for a selection nobody owns;
|
|
68
|
+
- `clear()` leaves the pasteboard alone, and `watch()` never fires.
|
|
69
|
+
|
|
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 —
|
|
79
|
+
can write `CLIPBOARD` itself: a surface's `selectedText()` is the text, and
|
|
80
|
+
`onSelectionChange` says when it moved.
|
|
81
|
+
|
|
82
|
+
## `useClipboard()`
|
|
83
|
+
|
|
84
|
+
Returns the clipboard for the connection this tree renders onto. Stable, so
|
|
85
|
+
it is safe in a dependency array. Everything on it returns a promise.
|
|
86
|
+
|
|
87
|
+
### Writing
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
await clipboard.writeText('hello');
|
|
91
|
+
await clipboard.write({
|
|
92
|
+
'text/html': '<b>hello</b>',
|
|
93
|
+
'text/plain': 'hello',
|
|
94
|
+
'image/png': pngBytes,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`write()` takes a map of **type name to payload** — strings are encoded
|
|
99
|
+
UTF-8, `Buffer`s and typed arrays are served as they are. Offering several
|
|
100
|
+
flavours of the same thing is the point: a rich-text editor takes the HTML,
|
|
101
|
+
a terminal takes the plain text, and each asks for what it understands.
|
|
102
|
+
There is no size limit worth naming; large payloads transfer incrementally.
|
|
103
|
+
|
|
104
|
+
`clear()` gives the selection back, so nothing is served for it any more.
|
|
105
|
+
|
|
106
|
+
### Reading
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const text = await clipboard.read('text'); // 'hello' | null
|
|
110
|
+
const files = await clipboard.readFiles(); // [{ uri, path? }]
|
|
111
|
+
const png = await clipboard.read('image/png'); // Uint8Array | null
|
|
112
|
+
const offered = await clipboard.targets(); // ['text/html', …]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**`read(type)` is the one to reach for.** It accepts a concrete type name or
|
|
116
|
+
a **group**, resolves it against what the owner actually offers, and decodes
|
|
117
|
+
text-ish types to a string. It answers `null` when the owner has nothing of
|
|
118
|
+
that kind, because "is there an image on the clipboard?" is a question, not
|
|
119
|
+
an error.
|
|
120
|
+
|
|
121
|
+
The same payload arrives under different names depending on who copied it,
|
|
122
|
+
so a group matches all of them:
|
|
123
|
+
|
|
124
|
+
| group | matches |
|
|
125
|
+
| --------- | ---------------------------------------------------------------------------------------------------------- |
|
|
126
|
+
| `'text'` | `text/plain;charset=utf-8`, `UTF8_STRING`, `text/plain`, `STRING`, `TEXT`, `COMPOUND_TEXT` (in that order) |
|
|
127
|
+
| `'files'` | `text/uri-list`, `application/x-kde4-urilist`, `x-special/gnome-copied-files` |
|
|
128
|
+
| `'uris'` | `text/uri-list`, `text/x-moz-url`, `_NETSCAPE_URL` |
|
|
129
|
+
|
|
130
|
+
This is the same table [drag and drop](drag-and-drop.md) uses. Groups do not
|
|
131
|
+
overlap — a Firefox link is
|
|
132
|
+
`text/x-moz-url`, which is in `uris`, not `text`.
|
|
133
|
+
|
|
134
|
+
On the Cocoa backend `targets()` lists the pasteboard's types in these
|
|
135
|
+
names: a type with a MIME type under that (`public.png` is `image/png`,
|
|
136
|
+
Finder's `public.file-url` is `text/uri-list`), text under both its X and
|
|
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.
|
|
145
|
+
|
|
146
|
+
`readFiles()` is `read('files')` with the RFC 2483 parsing applied: CRLF
|
|
147
|
+
separators, `#` comment lines, percent-decoding. `path` is present only for
|
|
148
|
+
genuinely local `file:` URIs — a remote `file://otherhost/…` gets a `uri`
|
|
149
|
+
and no `path` rather than a path that does not exist here.
|
|
150
|
+
|
|
151
|
+
`readText()` is the plain-text shortcut and asks only for `UTF8_STRING` then
|
|
152
|
+
`STRING`. It **rejects** when there is no owner or the owner offers neither,
|
|
153
|
+
where `read('text')` returns `null` and walks the whole preference list. If
|
|
154
|
+
you are not sure which you want, use `read('text')`.
|
|
155
|
+
|
|
156
|
+
### Knowing when it changes
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
let stop;
|
|
161
|
+
clipboard
|
|
162
|
+
.watch((ev) => setCanPaste(ev.owner !== 0))
|
|
163
|
+
.then((fn) => (stop = fn));
|
|
164
|
+
return () => stop?.();
|
|
165
|
+
}, [clipboard]);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The built-in edit menu on `<textinput>`/`<textarea>` already does this for
|
|
169
|
+
its Paste row — see [elements.md](elements.md#the-right-click-menu). This is
|
|
170
|
+
the same mechanism, for your own menus. If what you are building _is_ an edit
|
|
171
|
+
menu, open the standard one instead and the Paste row comes with it
|
|
172
|
+
([extending.md](extending.md#the-standard-edit-menu)).
|
|
173
|
+
|
|
174
|
+
`watch()` is a server-side subscription, not a poll — it costs nothing until
|
|
175
|
+
something changes. `owner === 0` means nothing is on the clipboard, which is
|
|
176
|
+
the case a Paste menu item wants. It rejects on a server without XFixes;
|
|
177
|
+
every X server since about 2004 has it.
|
|
178
|
+
|
|
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`.
|
|
186
|
+
|
|
187
|
+
## Timestamps
|
|
188
|
+
|
|
189
|
+
X arbitrates between two clients copying at the same moment by **timestamp**,
|
|
190
|
+
and ICCCM is explicit that the right one is the event that caused the copy —
|
|
191
|
+
never "now". You do not have to pass one: react-x11 remembers the last input
|
|
192
|
+
event on the connection and uses it, so a copy from a Ctrl+C handler is
|
|
193
|
+
stamped with that keystroke. Pass `{ time }` if you have a better answer.
|
|
194
|
+
|
|
195
|
+
This is also why a `write()` can fail: if another client copied with a newer
|
|
196
|
+
timestamp, the server refuses ours, and the promise rejects rather than
|
|
197
|
+
pretending.
|
|
198
|
+
|
|
199
|
+
### Owning a selection of your own
|
|
200
|
+
|
|
201
|
+
The same rule governs every other selection — a system tray takes
|
|
202
|
+
`_NET_SYSTEM_TRAY_S<screen>`, a clipboard manager takes `CLIPBOARD_MANAGER`,
|
|
203
|
+
an input method takes `XIM_SERVERS` — and there the timestamp is yours to
|
|
204
|
+
supply. Both of the values worth supplying are exported:
|
|
205
|
+
|
|
206
|
+
```jsx
|
|
207
|
+
import { lastInputTime, serverTime, useApp } from 'react-x11';
|
|
208
|
+
|
|
209
|
+
const app = useApp();
|
|
210
|
+
|
|
211
|
+
// the user did something, and ICCCM wants that event's own time
|
|
212
|
+
const take = (wid, selection) =>
|
|
213
|
+
app.X.SetSelectionOwner(wid, selection, lastInputTime(app));
|
|
214
|
+
|
|
215
|
+
// nothing did — a tray takes its selection when it starts
|
|
216
|
+
const claim = async (wid, selection) =>
|
|
217
|
+
app.X.SetSelectionOwner(wid, selection, await serverTime(app));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`lastInputTime(app)` is free: it is the timestamp of the last input event
|
|
221
|
+
this connection saw, which is what react-x11 already stamps copies with, and
|
|
222
|
+
what EWMH calls `_NET_WM_USER_TIME`. It is `undefined` before any input has
|
|
223
|
+
arrived.
|
|
224
|
+
|
|
225
|
+
`serverTime(app)` is one round trip and always answers. Reach for it when
|
|
226
|
+
there is no user action to name — and **do not** write `lastInputTime(app) ??
|
|
227
|
+
0` instead. Zero is `CurrentTime`, which ICCCM 2.1 forbids for exactly the
|
|
228
|
+
reason above: the server accepts it, two clients racing for one selection
|
|
229
|
+
cannot be ordered by it, and the loser is never told it lost.
|
|
230
|
+
|
|
231
|
+
`serverTime` resolves `0` if the server has not answered within five seconds.
|
|
232
|
+
It never rejects and never hangs.
|
|
233
|
+
|
|
234
|
+
## Reaching ntk directly
|
|
235
|
+
|
|
236
|
+
`useClipboard()` is a thin layer over ntk's `app.clipboard` and adds only the
|
|
237
|
+
vocabulary, the timestamps and the React access. Anything it does not cover
|
|
238
|
+
is one step away:
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
const app = useApp();
|
|
242
|
+
await app.clipboard.write(data, { selection: 'MY_SELECTION', time });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`app` is the whole ntk connection — `app.fonts`, `app.cursors`, `app.X` for
|
|
246
|
+
raw protocol. See ntk's own clipboard documentation for the ICCCM details.
|
|
247
|
+
|
|
248
|
+
## Limits
|
|
249
|
+
|
|
250
|
+
- **On the Cocoa backend `watch()` polls**, a quarter second behind a copy
|
|
251
|
+
made elsewhere, and a read of anything but text spawns `osascript` until
|
|
252
|
+
the bridge has `pasteboardItems`. `PRIMARY` is not a limit there but a
|
|
253
|
+
platform difference: see [`PRIMARY` is X11-only](#primary-is-x11-only).
|
|
254
|
+
- **No clipboard-manager handoff** (`SAVE_TARGETS`), so the data really does
|
|
255
|
+
vanish when the app exits.
|
|
256
|
+
- **`STRING` is latin-1 by definition**, so codepoints above U+00FF are lossy
|
|
257
|
+
in that target. Modern requestors ask for `UTF8_STRING`, which is not — and
|
|
258
|
+
a bare `writeText()` offers both.
|
|
259
|
+
- **No image decoding.** `read('image/png')` hands back the bytes; turning
|
|
260
|
+
them into something drawable is yours.
|
|
261
|
+
|
|
262
|
+
Design notes: [design/clipboard.md](../design/clipboard.md).
|