react-x11 2.51.1 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +9 -2
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
package/docs/system.md ADDED
@@ -0,0 +1,599 @@
1
+ # The machine around the app
2
+
3
+ Six hooks for the things an application has to read off the system rather than
4
+ work out for itself: the monitors, what the window manager did with the
5
+ window, whether anyone is at the desk, the state of the keyboard, how the
6
+ desktop wants an app to feel, and the language it was launched in — and a
7
+ store for what the app remembers itself, between launches.
8
+
9
+ ```jsx
10
+ import {
11
+ useScreens,
12
+ useWindowState,
13
+ useIdle,
14
+ useKeepAwake,
15
+ useKeyboardState,
16
+ useDesktopSettings,
17
+ createSettings,
18
+ useLocale,
19
+ } from 'react-x11';
20
+ ```
21
+
22
+ They share a shape, and it is worth knowing before reading any one of them:
23
+
24
+ - **Nothing is asked of the server until a component asks.** Every one of
25
+ these costs round trips or an event selection, and an app that never calls
26
+ them pays for none of it.
27
+ - **Every one has an answer with no extension present.** X is a protocol with
28
+ thirty years of optional pieces, and "this display cannot say" is a real
29
+ state rather than an error. Each hook below names what it reports then, and
30
+ which field tells that apart from a genuine "no".
31
+ - **The colours are somewhere else.** Light or dark, the accent and reduced
32
+ motion are [system appearance](appearance.md), which has a ladder of its own.
33
+
34
+ ---
35
+
36
+ ## `useScreens()` — the monitors
37
+
38
+ ```jsx
39
+ const { screens, primary, workArea, virtual, source } = useScreens();
40
+
41
+ <Select
42
+ value={monitor}
43
+ onChange={setMonitor}
44
+ options={screens.map((s) => ({
45
+ value: s.name,
46
+ label: `${s.name} — ${s.width}×${s.height}`,
47
+ }))}
48
+ />;
49
+ ```
50
+
51
+ Each entry in `screens`:
52
+
53
+ | | |
54
+ | ------------------------ | -------------------------------------------------------------- |
55
+ | `name` | `'HDMI-1'`, `'eDP-1'` — **null** where the server has no RandR |
56
+ | `x` `y` `width` `height` | the rect in virtual-screen coordinates |
57
+ | `available` | that rect minus the panels |
58
+ | `primary` | the desktop's main monitor |
59
+ | `widthMM` `heightMM` | physical size, or null |
60
+ | `refreshRate` | Hz to two decimals (`59.99`), or null |
61
+ | `rotation` | `0`, `90`, `180` or `270` |
62
+ | `outputs` | every output on this monitor — two names means it is mirrored |
63
+ | `scale` | this monitor's device pixels per logical pixel |
64
+
65
+ Re-renders when a monitor is plugged in or unplugged, when the arrangement
66
+ changes, and when a panel appears, moves or auto-hides.
67
+
68
+ The rects are in **logical pixels**, like every other length an app touches,
69
+ and `scale` is the per-monitor answer the display-scale ladder arrived at —
70
+ a retina lid and an office monitor genuinely differ. The root's own scale
71
+ is the primary monitor's; see [scale.md](scale.md).
72
+
73
+ ### The names arrive after the geometry
74
+
75
+ `source` says which tier answered. The geometry resolves during `createRoot()`
76
+ from **Xinerama**, which is one round trip; the names, the primary flag and the
77
+ physical sizes take a **RandR** walk — `GetScreenResourcesCurrent`, then a
78
+ `GetOutputInfo` and a `GetCrtcInfo` per output, ten or more round trips on an
79
+ ordinary two-head desktop — and that deliberately does not hold startup up.
80
+
81
+ So a component can render once with `source: 'xinerama'` and null names, and
82
+ again a moment later with `source: 'randr'` and real ones. The rects do not
83
+ move between the two: Xinerama on any modern server _is_ RandR's emulation of
84
+ it, so what arrives late is added rather than corrected. Where a name is what
85
+ gets persisted — "reopen on the monitor I was on" — treat null as _not known
86
+ yet_ and keep the last one rather than writing it.
87
+
88
+ `'screen'` is the tier under both: one entry covering the whole display, for a
89
+ server with neither extension. `null` is a headless mock with no display at
90
+ all.
91
+
92
+ ### `available` is an approximation on X11, and the only one here
93
+
94
+ `_NET_WORKAREA` — the desktop minus the docks and panels that reserved space —
95
+ is published **for the whole virtual desktop**, not per monitor. That is an
96
+ EWMH weakness rather than a choice made here. So it is applied as a _per-axis_
97
+ bound on top of the monitor rect: exact on one head, and on several it still
98
+ takes a top or bottom panel off the height without pretending to know which
99
+ head the panel is on.
100
+
101
+ A true per-monitor work area means reading `_NET_WM_STRUT_PARTIAL` off every
102
+ window on the screen and intersecting the reservations that land on each head
103
+ — a full window-tree walk, redone whenever any panel changes. If you need
104
+ that, `useApp().X` is the escape hatch.
105
+
106
+ ### On Wayland
107
+
108
+ The same shape, from `wl_output` ([wayland-backend.md](wayland-backend.md)):
109
+ `name` is the compositor's connector name (`DP-1`, `eDP-1`) and arrives with
110
+ the geometry rather than after it; the rects are xdg_output's logical ones.
111
+ The protocol has no primary — the entry at the origin is flagged — and no
112
+ work area: `available` is each monitor's own rect, less what the compositor
113
+ told a window on it to fit in (`xdg_toplevel.configure_bounds`, the monitor
114
+ minus its panels), so it is exact per monitor, as on macOS, once a window has
115
+ appeared there. `scale` is the monitor's own, the fraction under fractional
116
+ scaling. `source` reads `'test'` on both native backends — the stamp of the
117
+ seam they publish through.
118
+
119
+ **On macOS it is exact.** `NSScreen.visibleFrame` is per screen already — that
120
+ display's own menu bar and Dock taken off — so each monitor carries its real
121
+ usable rect and no approximation runs. `workArea` there is still one rect,
122
+ because the field is one rect by definition; it is the primary screen's, and
123
+ on a multi-display Mac `screens[n].available` is the answer to prefer.
124
+ `primary` is the screen with the menu bar, `refreshRate` the panel's own rate,
125
+ and `name` is null — the bridge does not report one.
126
+
127
+ ### On macOS the layout is asked for, not announced
128
+
129
+ Every other backend is told: RandR sends an event, a `wl_output` announces
130
+ itself. macOS has no such event for a client — the bridge keeps its
131
+ `NSScreen` copy current on the system's own notification and says nothing —
132
+ so react-x11 re-reads the layout instead, at the moments it matters:
133
+
134
+ - **before a placement**, which is where a stale rect would be visible — a
135
+ popup flipped and clamped into a monitor that has moved, an auto-sized
136
+ window capped by one that is gone;
137
+ - **when a window reports that it moved**, at most once a second, because a
138
+ display arriving or leaving moves the windows that were on it;
139
+ - **on a clock while a `useScreens()` subscriber is mounted**, every
140
+ `createRoot({ cocoa: { screenPoll } })` ms — 500 by default, `0` for
141
+ never. An app that never calls `useScreens()` never polls.
142
+
143
+ So a component watching the layout sees a monitor plugged in about half a
144
+ second later than an X11 one does, and everything else sees it at the moment
145
+ it asks. `useApp().refreshScreens()` publishes the layout right now for an
146
+ app that knows before any of those.
147
+
148
+ ### Mirroring
149
+
150
+ Two outputs showing the same pixels share one CRTC and are reported as **one**
151
+ monitor, with both names in `outputs`. A laptop mirroring to a projector is one
152
+ screen to put a window on, and `screens.length === 2` there would be a wrong
153
+ answer an app would act on.
154
+
155
+ ---
156
+
157
+ ## `useWindowState()` — what the window manager did
158
+
159
+ ```jsx
160
+ const { focused, visible, fullscreen } = useWindowState();
161
+
162
+ // a title bar that dims when the window is not the active one
163
+ <text style={{ color: focused ? theme.text : theme.textMuted }}>{title}</text>;
164
+
165
+ // and work that is pointless while nobody can see it
166
+ useEffect(() => {
167
+ if (!visible) return;
168
+ const id = setInterval(poll, 1000);
169
+ return () => clearInterval(id);
170
+ }, [visible]);
171
+ ```
172
+
173
+ | | |
174
+ | ------------ | ---------------------------------------------------------------------- |
175
+ | `focused` | this window has the keyboard |
176
+ | `visible` | **the one to branch on** — not minimized, not covered, still drawing |
177
+ | `minimized` | `_NET_WM_STATE_HIDDEN`: iconified, or shaded away |
178
+ | `maximized` | both axes; one axis alone shows up in `states` |
179
+ | `fullscreen` | what the WM actually did, not what `<window fullscreen>` asked for |
180
+ | `obscured` | fully covered — **always false under a compositor** |
181
+ | `presenting` | the display is still scheduling frames — Wayland; true everywhere else |
182
+ | `states` | the raw `_NET_WM_STATE` names |
183
+ | `desktop` | the workspace index, or null |
184
+
185
+ `<window states>` is what a window **asks for**; this is what it **got**. The
186
+ two part company routinely — a user hits a maximize hotkey, a tiling WM
187
+ declines a fullscreen request — and an app that mirrors the state in its own
188
+ UI has nowhere else to read it.
189
+
190
+ On the **Cocoa** backend the same rows come from AppKit, in the same
191
+ `_NET_WM_STATE` names (@windowkit/appkit 0.30.0): `fullscreen` is the title
192
+ bar's green button, or `<window fullscreen>`, `maximized` the zoom
193
+ (`maximized_vert` and `maximized_horz` together), `minimized` the Dock
194
+ (`hidden`), and `states` lists `focused` for the key window. `fullscreen`
195
+ turns as AppKit's transition **begins**, before the animation, so a toolbar an
196
+ app hides in full screen goes while the window grows rather than after it:
197
+
198
+ ```jsx
199
+ const { fullscreen } = useWindowState();
200
+ return fullscreen ? <Page /> : [<Toolbar />, <Page />];
201
+ ```
202
+
203
+ `obscured` and `desktop` keep their initial values there — a Space is not a
204
+ numbered workspace — and on a bridge older than 0.30.0 only `focused` moves.
205
+
206
+ With no argument it reads the window the component is in, inferred the way
207
+ [`useTopLevelWindow()`](filedialog.md) infers it: exact for a one-window app,
208
+ and a documented guess for a tree with several. Pass a ref to be certain.
209
+
210
+ ```jsx
211
+ const win = useRef(null);
212
+ const { fullscreen } = useWindowState(win);
213
+ return <window ref={win}>…</window>;
214
+ ```
215
+
216
+ The first render answers `focused: true, visible: true` rather than waiting for
217
+ a round trip. A window opens focused and on screen far more often than not, and
218
+ a title bar that renders dimmed on frame one and un-dims on frame two is a
219
+ flash on every launch.
220
+
221
+ ### `obscured` and the compositor
222
+
223
+ **A composited window is never obscured.** The compositor redirects it to an
224
+ offscreen pixmap, so the server considers it entirely visible whatever is
225
+ stacked on top, and `VisibilityNotify` says so. One is running on every stock
226
+ GNOME and KDE session. That is X working as designed and there is no protocol
227
+ answer to it.
228
+
229
+ So `obscured` is a bare-WM optimisation, and `visible` — which folds in
230
+ `minimized`, the signal that _does_ survive compositing — is the field to
231
+ branch on. An animation paused on `visible` stops when the window is minimized
232
+ everywhere, and additionally when it is buried on a desktop with no compositor.
233
+
234
+ ### `presenting`, and the Wayland frame clock
235
+
236
+ A Wayland compositor will not tell you a surface is covered either — but it
237
+ stops sending it **frame callbacks**, and on that backend a frame callback is
238
+ the only thing that paces the frame loop. So a window nobody is showing draws
239
+ once and then stops, while React above it goes on re-rendering into a tree
240
+ that reaches no screen. That is the compositor being right (painting into a
241
+ surface nobody sees is work for nothing) and it used to be invisible: no
242
+ error, no warning, and nothing an app could ask.
243
+
244
+ `presenting` is that state, published. It goes false after a couple of
245
+ seconds of silence with a repaint waiting, and true again on the very next
246
+ frame callback — the compositor still holds the one we asked for, and answers
247
+ it the moment the surface is shown again. It folds into `visible`, so an
248
+ animation already paused on `visible` pauses here too:
249
+
250
+ ```jsx
251
+ const { visible, presenting } = useWindowState();
252
+
253
+ // nothing this draws would reach the screen; stop simulating as well
254
+ useEffect(() => {
255
+ if (!visible) return;
256
+ const id = setInterval(step, 16);
257
+ return () => clearInterval(id);
258
+ }, [visible]);
259
+
260
+ // and the reason, where a status line would rather say it than go blank
261
+ const status = presenting ? 'live' : 'paused — this window is not on screen';
262
+ ```
263
+
264
+ It is a signal to **stop work** on, not a frame-accurate one: two seconds of
265
+ silence is what makes it certain, so nothing that has to be exact should read
266
+ it. On X11, macOS and the headless mock it is `true` and stays there — their
267
+ frame clocks do not depend on a compositor's goodwill, and `obscured` is the
268
+ question to ask there instead. The backend also says it once on stderr in
269
+ development, because "the app is alive and has drawn once" is otherwise an
270
+ unguessable state; see
271
+ [wayland-backend.md](wayland-backend.md#a-surface-the-compositor-stops-showing-stops-drawing).
272
+
273
+ ---
274
+
275
+ ## `useIdle()` and `useKeepAwake()` — is anyone there
276
+
277
+ ```jsx
278
+ const away = useIdle(5 * 60_000);
279
+ <Avatar status={away ? 'away' : 'online'} />;
280
+ ```
281
+
282
+ Idleness is the **whole display's**, not this window's: it counts input on
283
+ every device, whichever application it went to. That is the right meaning for a
284
+ presence indicator, an auto-save, or a dashboard that stops refreshing when the
285
+ desk is empty — and the wrong one for "has the user ignored _my_ window", which
286
+ is `useWindowState().focused`.
287
+
288
+ Where the X server carries an `IDLETIME` counter — Xorg does — this costs **no
289
+ timer at all**: a SYNC alarm fires when the counter crosses `timeout` and
290
+ another when input pulls it back down, which is what every idle daemon on the
291
+ desktop is built on. On a server without one (XQuartz) it falls back to polling
292
+ MIT-SCREEN-SAVER, scheduled against the remaining time rather than on a tick —
293
+ and on win32, `GetLastInputInfo` on the same schedule, which answers for the
294
+ whole session the way the X counter does. On macOS it is the same schedule
295
+ over the session's idle time: the `@windowkit/appkit` bridge's `lastInputMs()`
296
+ (CoreGraphics' count, a call) where it has one, and otherwise `ioreg`'s
297
+ `HIDIdleTime` — a short-lived child process per check, which is one per
298
+ `timeout` while the user is there and one every quarter of it, at most every
299
+ half minute, while they are away.
300
+ On an X display with neither extension it stays `false`.
301
+
302
+ Nothing is armed until a component subscribes, and the alarm or the poll
303
+ stops when the last subscriber to that `timeout` unmounts.
304
+
305
+ `timeout` is a dependency, so hold it in a constant rather than building it
306
+ inline from state that moves.
307
+
308
+ ### Keeping the screen on
309
+
310
+ ```jsx
311
+ useKeepAwake(playing, 'Playing a video');
312
+ ```
313
+
314
+ Held while the flag is true and the component is mounted, released on either —
315
+ including on an unmount mid-playback, which is the case that leaves a desktop
316
+ permanently un-blanking when this is done by hand.
317
+
318
+ Four rungs. On [win32](windows-integrations.md), `SetThreadExecutionState`
319
+ with `ES_CONTINUOUS`, which is what makes it a state rather than a one-shot
320
+ nudge, and which is counted here so that one caller finishing does not speak
321
+ for another. Elsewhere: the settings portal's `Inhibit`, the older
322
+ `org.freedesktop.ScreenSaver` service, and `ScreenSaverSuspend`, which is a
323
+ plain X request and needs no session bus at all. **The three freedesktop
324
+ rungs inhibit screen blanking only.** Nothing there stops a suspend — only
325
+ the portal rung could, and a seam that works on one desktop in three is worse
326
+ than not offering it. The Windows rung asks for both the display and the
327
+ system, which is the same call either way.
328
+
329
+ `reason` is shown by desktops that list what is holding the screen on, so it
330
+ reads best as a sentence about the app's state rather than the app's name,
331
+ which they already show. Windows takes no string with the call —
332
+ `powercfg /requests` names the process — so there it is dropped rather than
333
+ put somewhere the system will not show it.
334
+
335
+ Failure is silent by design: a machine with no portal, no screensaver service
336
+ and no MIT-SCREEN-SAVER cannot be asked, and a video player is not the place to
337
+ surface that. `keepAwake()` is the imperative twin, and resolves to the release.
338
+
339
+ ---
340
+
341
+ ## `useKeyboardState()` — the locks and the layout
342
+
343
+ ```jsx
344
+ const { capsLock, layout } = useKeyboardState();
345
+
346
+ <PasswordInput value={password} onChange={setPassword} />;
347
+ {
348
+ capsLock && <text style={{ color: theme.warning }}>Caps Lock is on</text>;
349
+ }
350
+ ```
351
+
352
+ | | |
353
+ | -------------------- | ------------------------------------------ |
354
+ | `capsLock` `numLock` | on or off, **now** |
355
+ | `group` | the active XKB group, `0`–`3` |
356
+ | `layout` | that group's layout code, `'ru'` — or null |
357
+ | `layouts` | every configured layout, `['us', 'ru']` |
358
+
359
+ The point of the first row is that it is true of the **keyboard** rather than
360
+ of an event. A key event carries the modifier state at the moment it was
361
+ pressed, so an app can see that Caps Lock was on for a key it received — which
362
+ is no help at all in the case that matters, a password field that wants to warn
363
+ while it is merely focused and before anything has been typed.
364
+
365
+ The layout half is for a status-bar indicator, and for the other thing that
366
+ costs people real time: a password typed in the wrong script, which looks
367
+ identical to a wrong password. `layout` is a code rather than a description
368
+ (`'ru'`, not `'Russian'`) because that is what an indicator shows; uppercase it
369
+ for display.
370
+
371
+ Everything here needs **XKB**, which Xorg and XQuartz both have. Where it is
372
+ missing the locks read false and `layouts` is empty — so `layouts` is the field
373
+ to check when the difference between "off" and "not known" matters. Changes
374
+ arrive as `XkbStateNotify`, with no polling, even while another application has
375
+ the keyboard.
376
+
377
+ The [native Wayland backend](wayland-backend.md) answers the same fields from
378
+ its seat: the locks and the group from `wl_keyboard.modifiers`, the layout
379
+ codes from the keymap the compositor sent. One difference is the protocol's
380
+ rather than this library's — a compositor tells only the focused client, so a
381
+ lock toggled while another app has the keyboard is seen when focus returns.
382
+ For a password field, the case this hook exists for, the window is focused
383
+ already.
384
+
385
+ Which bit is which lock is worth writing down: Caps Lock is `LockMask`, fixed
386
+ by the core protocol, and **Num Lock is not fixed by anything** — it is wherever
387
+ the modifier map puts it, which is `Mod2` on every Linux and BSD desktop, on
388
+ XQuartz, and in every toolkit that hardcodes it. This takes the convention.
389
+
390
+ ---
391
+
392
+ ## `useDesktopSettings()` — how the desktop wants an app to feel
393
+
394
+ ```jsx
395
+ const { doubleClickMs, dragThreshold } = useDesktopSettings();
396
+ ```
397
+
398
+ | | |
399
+ | --------------------- | -------------------------------------------------------------------- |
400
+ | `caretBlink` | **false means do not blink at all** |
401
+ | `caretBlinkMs` | how long a caret stays in each state (530) |
402
+ | `doubleClickMs` | how long a second click has to arrive in (400) |
403
+ | `doubleClickDistance` | and how far it may land from the first (4px) |
404
+ | `dragThreshold` | how far a press moves before it is a drag (4px) |
405
+ | `animations` | **false means reduce motion** — see below |
406
+ | `source` | `'xsettings'`, `'portal'`, `'macos'`, or null where nothing answered |
407
+
408
+ **The built-in controls already follow these.** `<textinput>`'s caret, the click
409
+ counting behind `ev.detail`, and the drag threshold all read the same values, so
410
+ an app that uses the widget set gets this for nothing. The hook is for an app
411
+ drawing its own: a `<canvas>` with a text cursor in it, a custom gesture, a
412
+ diagram editor with its own drag. Reach for it so that one widget on the screen
413
+ does not feel unlike every other.
414
+
415
+ `animations` is the desktop's "reduce motion", from GTK's
416
+ `gtk-enable-animations`. **Looping animations already honour it** — a
417
+ [`style.animation`](styling.md#loops) never starts while it is false, so
418
+ `<ProgressBar indeterminate>` and anything else built on the loop primitive
419
+ comes to rest with nothing to do. Transitions are left alone: a 120ms fade
420
+ that ends is not the unbounded motion the setting exists for. Read the field
421
+ where the _resting_ look is a design decision your app has to make — a
422
+ spinner that does not spin should still say it is working, which is what
423
+ `<ProgressBar indeterminate>` does by filling its track instead.
424
+
425
+ `caretBlink` deserves the callout. A caret that ignores it is not a cosmetic
426
+ miss: a moving thing on screen is what some people cannot read past, which is
427
+ why the desktop offers the switch at all. **Draw the caret solid rather than not
428
+ at all** — that is what the built-in controls do.
429
+
430
+ [XSETTINGS](appearance.md#what-each-rung-can-actually-answer) is the only source on X11; there is no
431
+ standard portal interface for any of this. On the
432
+ [native Wayland backend](wayland-backend.md), which has no XSETTINGS to read,
433
+ they come off the settings portal's GNOME namespaces instead —
434
+ `org.gnome.desktop.interface` (`cursor-blink`, `cursor-blink-time`,
435
+ `enable-animations`) and `org.gnome.desktop.peripherals.mouse`
436
+ (`double-click`, `drag-threshold`), the same GSettings keys gsd-xsettings
437
+ translates for X11 — with `source: 'portal'`, live, and only while
438
+ `createRoot({ desktop })` leaves `appearance` on. A portal that does not pass
439
+ those through (KDE's, a bare compositor's) leaves the defaults. On the macOS backend the one of these the
440
+ system publishes is read from it: **Reduce motion** (System Settings ›
441
+ Accessibility › Display) arrives as `animations: false` with
442
+ `source: 'macos'`, live — flipping the switch stops a spinner that is already
443
+ going round — and the other fields keep the defaults there, since macOS has
444
+ no equivalent to hand over. On a desktop with no settings daemon — a bare
445
+ `startx`, most window managers, XQuartz — `source` is null and the numbers above
446
+ are this renderer's own defaults. They are deliberately not GTK's, which blinks
447
+ on a 1200ms cycle and drags at 8px: the point is to follow a desktop that
448
+ expressed a preference, not to change what happens on one that did not.
449
+
450
+ One seam worth knowing about. `createRoot()` starts XSETTINGS but does not
451
+ **await** it — five round trips on every app's startup path, to answer a
452
+ question that is not asked until the first interaction, is a bad trade. A field
453
+ focused on the very first frame therefore gets the built-in default and the
454
+ desktop's cadence from its next focus onward.
455
+
456
+ ---
457
+
458
+ ## `createSettings()` — what the app remembers
459
+
460
+ The desktop's settings are read above; an app's own — the volume it was left
461
+ at, the theme it was switched to — are kept here, in one JSON file per app in
462
+ the per-user directory for it:
463
+
464
+ ```jsx
465
+ import { createSettings } from 'react-x11';
466
+
467
+ const settings = createSettings({
468
+ appId: 'com.example.hush',
469
+ defaults: { noiseType: 'brown', volume: 0.5, dark: false },
470
+ });
471
+
472
+ function Volume() {
473
+ const [volume, setVolume] = settings.use('volume');
474
+ return <Slider value={volume} onChange={setVolume} />;
475
+ }
476
+ ```
477
+
478
+ `settings.use(key)` is a `useState` every component shares: every component
479
+ using the key, in any window, sees one value, and setting it from one moves
480
+ them all. It takes an updater function too, and `settings.use(key, fallback)`
481
+ covers a key with no default.
482
+
483
+ | platform | file |
484
+ | --------- | -------------------------------------------------------- |
485
+ | macOS | `~/Library/Application Support/<appId>/settings.json` |
486
+ | elsewhere | `$XDG_CONFIG_HOME/<appId>/settings.json` (`~/.config/…`) |
487
+
488
+ The `appId` is a reverse-DNS name no other app uses, the shape
489
+ [`registerApplication`](uri-schemes.md) takes; the store registers nothing
490
+ with it. Creating the store twice with the same id, in a module a hot reload
491
+ evaluates again or in two call sites, is one store with one set of values.
492
+
493
+ **It is read synchronously, the first time a value is asked for**, so the
494
+ first render already has what was saved rather than the defaults and then a
495
+ jump. It is written asynchronously:
496
+
497
+ - **coalesced** — a quarter second after the last change, and at least once a
498
+ second while changes keep coming, so a drag on a slider is a few writes and
499
+ not one per pointer move;
500
+ - **atomically** — into a temporary file that is then renamed over the old
501
+ one, so a crash mid-write leaves the file as it was, never half a file;
502
+ - **on exit** — whatever is still unsaved when the process exits is written
503
+ on the way out, including a change whose write was in flight. `await
504
+ settings.flush()` writes it now.
505
+
506
+ Outside React, `settings.get(key)`, `settings.set(key, value)`,
507
+ `settings.reset(key)` — the default again — and `settings.subscribe(listener)`
508
+ are the same store. A value is whatever JSON can keep, and one it cannot, a
509
+ function or a `BigInt`, is a `TypeError` at `set`. A file that is not JSON is
510
+ reported once and the defaults stand; the next change replaces it.
511
+
512
+ Two copies of the app running at once share the file, and the last write
513
+ wins: nothing watches for another process changing it.
514
+
515
+ ---
516
+
517
+ ## `useLocale()` — the language and its conventions
518
+
519
+ ```jsx
520
+ const { locale, weekStartsOn } = useLocale();
521
+
522
+ // a date grid — `@react-x11/components`' <Calendar> takes both
523
+ <Calendar locale={locale} weekStartsOn={weekStartsOn} />;
524
+ <text>{new Intl.NumberFormat(locale).format(total)}</text>;
525
+ ```
526
+
527
+ | | |
528
+ | -------------- | ----------------------------------------------------------- |
529
+ | `locale` | a BCP-47 tag: `'en-GB'`, `'ru-RU'` |
530
+ | `direction` | `'ltr'` or `'rtl'` |
531
+ | `weekStartsOn` | `0` Sunday … `6` Saturday, from CLDR |
532
+ | `timeZone` | `'Europe/London'`, or null |
533
+ | `source` | `'env'` when `LANG` and friends said, `'intl'` when ICU did |
534
+
535
+ **`LC_ALL` / `LC_MESSAGES` / `LANG` win over `Intl`'s own answer.** node resolves
536
+ `Intl.DateTimeFormat().resolvedOptions().locale` through ICU, and ICU answers
537
+ what it has compiled in — a small-ICU build reports `en-US` for every
538
+ environment there is. The POSIX variables are what the person who launched the
539
+ app actually said. `C` and `POSIX` are not locales but the _absence_ of one, so
540
+ they fall through to ICU rather than becoming a tag.
541
+
542
+ **This does not change while the app runs**, and there is deliberately no
543
+ subscription behind it: a process's environment is fixed at exec and ICU's
544
+ resolution with it, so a desktop that switches language announces it for the
545
+ _next_ login and nothing this process can observe has moved.
546
+
547
+ For text direction inside a component prefer `useDirection()` over
548
+ `useLocale().direction` — it also honours `<ThemeProvider direction>` and the
549
+ `direction` style property, which the locale knows nothing about. See
550
+ [styling](styling.md). The default theme is already seeded from the locale, so
551
+ an app started under `LANG=ar_EG.UTF-8` mirrors without being told to.
552
+
553
+ ---
554
+
555
+ ## What is not here
556
+
557
+ **Network status.** There is no X-level answer, so it is necessarily D-Bus or
558
+ netlink, and the honest version of the API would be a hint rather than a fact —
559
+ a failed request is always the better truth than a flag that says the link is
560
+ up. Not ruled out; not built.
561
+
562
+ **Battery, brightness, volume, media keys.** These are D-Bus services a third
563
+ party can wrap without anything from here. Two things once on this list — the
564
+ two desktop standards with a claim on core — are now built: **notifications**
565
+ ([notifications.md](notifications.md)), and the **tray**, whose menu is
566
+ [dbusmenu](globalmenu.md) this renderer already speaks — on the cocoa backend
567
+ [`useTray()`](desktop.md) over `NSStatusItem`
568
+ ([#463](https://github.com/sidorares/react-x11/issues/463)), and on a
569
+ freedesktop session over `org.kde.StatusNotifierItem`
570
+ ([#353](https://github.com/sidorares/react-x11/issues/353)) — D-Bus only, so
571
+ the same rung serves X11, XWayland and Wayland — and on win32 over
572
+ `Shell_NotifyIcon`.
573
+
574
+ **The user's calendar.** Their real events, from the accounts the desktop
575
+ already holds — EventKit on macOS, Evolution Data Server on a freedesktop
576
+ session — are [desktop-calendar.md](desktop-calendar.md):
577
+ `useDesktopCalendarEvents()`, keyed the way a calendar grid asks. It is not a
578
+ system _setting_, which is why it is its own page rather than a hook here,
579
+ but it is the same kind of thing as the rest of this one: something the
580
+ desktop knows that an app would otherwise ask the user to type in again.
581
+
582
+ **A launcher badge, and permission prompts.** Both are built, and neither is
583
+ a system hook: an unread count on the icon (freedesktop
584
+ `com.canonical.Unity.LauncherEntry`, one D-Bus signal; macOS
585
+ `dockTile.badgeLabel`; a taskbar overlay icon on Windows) is `useBadge()` in
586
+ [desktop.md](desktop.md#the-badge), and camera/mic/location authorization is
587
+ `usePermission()` in [permissions.md](permissions.md) — macOS TCC today, with
588
+ the freedesktop device portals still
589
+ [#466](https://github.com/sidorares/react-x11/issues/466)'s remaining half. Attention (the taskbar urgency / Dock
590
+ bounce) is `states: ['demands_attention']`
591
+ ([desktop.md](desktop.md#attention)).
592
+
593
+ **Session lifecycle** — "we are suspending, save now", and "I have unsaved work,
594
+ do not log out yet". The X-native answer (XSMP) is effectively dead and the live
595
+ one is logind. The shape worth having is probably not a system hook but a
596
+ `useBeforeQuit()` that unifies it with `<window onCloseRequest>`. On the cocoa
597
+ backend the second half already lands there: a quit request from the Dock,
598
+ ⌘Q or a logout is routed to the primary window's `onCloseRequest`
599
+ ([uri-schemes.md](uri-schemes.md), "On macOS").