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.
- package/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +9 -2
- 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/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -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/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").
|