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
package/docs/remote.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Remote display
|
|
2
|
+
|
|
3
|
+
A react-x11 app **draws over the wire**. It does not render pixels and ship
|
|
4
|
+
them; it sends X11 drawing requests — rectangles, glyph runs, composites —
|
|
5
|
+
and the X server on the far end turns them into pixels. So running the app
|
|
6
|
+
somewhere else is not a feature bolted on top: it is what the protocol is,
|
|
7
|
+
and the program does not know the difference.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
ssh -X buildbox
|
|
11
|
+
npm start # the window opens on your desktop
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Nothing in the app changes. No VNC, no browser, no streaming daemon, no port
|
|
15
|
+
to forward, no second copy of the UI.
|
|
16
|
+
|
|
17
|
+
**This is the X11 backend**, and it is the reason X11 stays the flagship
|
|
18
|
+
target: the Cocoa, win32 and Wayland backends have no wire at all, so
|
|
19
|
+
nothing on this page applies to them. That matters in one case — **an app
|
|
20
|
+
running on a Mac or on Windows and drawing somewhere else**. `$DISPLAY` in
|
|
21
|
+
the environment does not choose the backend there; `backend: 'auto'` sees
|
|
22
|
+
darwin and picks Cocoa, or win32 and picks the win32 backend, and the window
|
|
23
|
+
opens on the local machine. Say which you want:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
REACT_X11_BACKEND=x11 DISPLAY=thin-client:0 npm start
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`createRoot({ display: … })` or `createRoot({ stream: … })` also settle it —
|
|
30
|
+
naming an X endpoint in code _is_ choosing X11, because an ignored
|
|
31
|
+
connection option would be worse than either answer. An app running on Linux
|
|
32
|
+
and drawing to a Mac's XQuartz is not affected: both ends are X11 already.
|
|
33
|
+
|
|
34
|
+
## What it costs, measured
|
|
35
|
+
|
|
36
|
+
Bandwidth is small: **a whole window appears for about four kilobytes** of
|
|
37
|
+
requests, and scrolling a 500-row table ten notches costs 11 kB, because
|
|
38
|
+
only the damaged strip is redrawn — where a single frame of pixel streaming
|
|
39
|
+
(VNC, RDP) is tens to hundreds of kilobytes, sent again whenever anything
|
|
40
|
+
moves.
|
|
41
|
+
|
|
42
|
+
What react-x11 pays instead is **latency**. A request that waits for a
|
|
43
|
+
reply with nothing pipelined behind it stalls the connection for a full
|
|
44
|
+
round trip, so on a 60 ms link every such reply is 60 ms of nothing. That is
|
|
45
|
+
why atoms, geometry and glyph pages are cached rather than re-asked, and
|
|
46
|
+
why the column to watch on a slow link is `stalls`, not `replies`:
|
|
47
|
+
`npm run bench -- --hotspots` attributes them, request by request.
|
|
48
|
+
|
|
49
|
+
Rules of thumb, reasoned from the request and reply counts rather than
|
|
50
|
+
measured over a shaped link:
|
|
51
|
+
|
|
52
|
+
- **Under ~20 ms RTT** (a LAN, a nearby VPS): indistinguishable from local.
|
|
53
|
+
- **20–80 ms** (a continent away): typing and scrolling stay responsive;
|
|
54
|
+
window mapping and font setup have a visible beat.
|
|
55
|
+
- **Above ~150 ms** or on a lossy link: X11's chattiness starts to show, and
|
|
56
|
+
a pixel-streaming protocol designed for high latency will feel better.
|
|
57
|
+
This is X11's limit, not react-x11's.
|
|
58
|
+
|
|
59
|
+
The per-scenario numbers are in the
|
|
60
|
+
[design notes](../design/remote.md#what-it-costs-measured).
|
|
61
|
+
|
|
62
|
+
## `ssh -X` and `ssh -Y`
|
|
63
|
+
|
|
64
|
+
`-X` runs your app as an **untrusted** X client. `-Y` runs it trusted, which
|
|
65
|
+
turns the restrictions off. They are not "the one that works" and "the one
|
|
66
|
+
that does not":
|
|
67
|
+
|
|
68
|
+
| | `-X` (untrusted) | `-Y` (trusted) |
|
|
69
|
+
| ---------------------------------- | ---------------- | -------------- |
|
|
70
|
+
| read other windows' pixels | no | yes |
|
|
71
|
+
| grab the keyboard, record input | no | yes |
|
|
72
|
+
| take the clipboard from other apps | restricted | yes |
|
|
73
|
+
| your app's own windows | fine | fine |
|
|
74
|
+
|
|
75
|
+
**Prefer `-X`.** react-x11 should work under it — it draws into windows it
|
|
76
|
+
created, which is what untrusted mode is for. If something does not, that is
|
|
77
|
+
a bug worth filing rather than a reason to reach for `-Y`, which hands every
|
|
78
|
+
program on the remote host the ability to read your screen and log your
|
|
79
|
+
keystrokes.
|
|
80
|
+
|
|
81
|
+
### The failure that looks like a crash
|
|
82
|
+
|
|
83
|
+
An untrusted `-X` cookie **expires**. OpenSSH's `ForwardX11Timeout` defaults
|
|
84
|
+
to 20 minutes; after that the connection is refused and your app dies
|
|
85
|
+
mid-session with a connection reset — no warning, no message that mentions
|
|
86
|
+
X11, and it happens long enough after startup that nobody connects the two.
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
# in ~/.ssh/config
|
|
90
|
+
Host buildbox
|
|
91
|
+
ForwardX11 yes
|
|
92
|
+
ForwardX11Trusted no
|
|
93
|
+
ForwardX11Timeout 8h # or 0 for no expiry
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If a long-running app dies after roughly twenty minutes and
|
|
97
|
+
`onDisconnect(reason)` reports `'error'`, this is the first thing to check.
|
|
98
|
+
react-x11 tells you it happened, and nothing more —
|
|
99
|
+
[a reconnect is not a reconnect](README.md):
|
|
100
|
+
every window id, pixmap, glyph set and font died with the connection.
|
|
101
|
+
|
|
102
|
+
## `DISPLAY=host:0` — and why not
|
|
103
|
+
|
|
104
|
+
The other way to reach a remote display is to point `DISPLAY` straight at it
|
|
105
|
+
and open port 6000. Do not:
|
|
106
|
+
|
|
107
|
+
- The connection is **plaintext**. Every keystroke, every glyph you draw and
|
|
108
|
+
the authentication cookie itself cross the network in the clear.
|
|
109
|
+
- Making it work usually means `xhost +`, which turns off access control for
|
|
110
|
+
_everyone_, or `xhost +host`, which trusts every user on that host.
|
|
111
|
+
- Most X servers ship with TCP listening disabled now, so it takes
|
|
112
|
+
deliberate work to open the hole.
|
|
113
|
+
|
|
114
|
+
`ssh -X` gives you the same thing, encrypted, authenticated per-session, and
|
|
115
|
+
with no listening port. Use it. If you genuinely need a raw display
|
|
116
|
+
connection — a lab network, a kiosk — carry the cookie explicitly with
|
|
117
|
+
`xauth` and never with `xhost` (see [security.md](security.md)).
|
|
118
|
+
|
|
119
|
+
## The other X servers
|
|
120
|
+
|
|
121
|
+
"An X server" is broader than "a Linux desktop":
|
|
122
|
+
|
|
123
|
+
| where | what runs |
|
|
124
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| Linux desktop | X.Org, or **Xwayland** — see below |
|
|
126
|
+
| macOS | [XQuartz](https://www.xquartz.org). `ssh -X` from Terminal starts it for you |
|
|
127
|
+
| Windows | VcXsrv, Xming, or an X server inside WSLg |
|
|
128
|
+
| headless CI | `Xvfb :99 -screen 0 1280x800x24` |
|
|
129
|
+
| a disposable screen | `Xephyr :10 -screen 1200x800` — a window on your desktop that _is_ a display. What the [window-manager example](../examples/README.md) needs |
|
|
130
|
+
| a session you can reattach to | `Xvnc` / TigerVNC, or **Xpra**, which is X11-native and can detach and reattach a running app |
|
|
131
|
+
| no display at all | node-x11's in-process server — [testing.md](testing.md) |
|
|
132
|
+
|
|
133
|
+
**Xwayland: yes.** On a Wayland desktop, X11 clients run under Xwayland, and
|
|
134
|
+
react-x11 is an ordinary X11 client there. Everything in this documentation
|
|
135
|
+
applies. Some window-manager interactions are thinner (Xwayland brokers what
|
|
136
|
+
the compositor is willing to expose), and `WM_TRANSIENT_FOR` and the
|
|
137
|
+
`_NET_WM_STATE` work are among the things that survive the trip — see
|
|
138
|
+
[elements.md](elements.md#transientfor--a-window-that-belongs-to-another).
|
|
139
|
+
|
|
140
|
+
**Native Wayland: not remotely.** Wayland has no drawing protocol: a client
|
|
141
|
+
renders pixels into a buffer itself and hands the compositor a handle, so
|
|
142
|
+
there is no drawing to send over a wire. The
|
|
143
|
+
[Wayland backend](wayland-backend.md) rasterizes locally, which is why it is
|
|
144
|
+
opt-in and never what `'auto'` picks: on Linux the default stays X11, and
|
|
145
|
+
with it everything on this page.
|
|
146
|
+
|
|
147
|
+
**XQuartz notes**, since it is the most common macOS path:
|
|
148
|
+
|
|
149
|
+
- The window manager is quartz-wm, which has the thinnest EWMH support in
|
|
150
|
+
the stack. It does not advertise `_NET_WM_STATE_ABOVE`, so `alwaysOnTop`
|
|
151
|
+
falls back to Apple-WM window levels ([elements.md](elements.md)).
|
|
152
|
+
- Put `/opt/X11/bin` first on `PATH`. Font family resolution shells out to
|
|
153
|
+
`fc-match`, and Homebrew's fontconfig — which is usually found first —
|
|
154
|
+
ships no macOS system-font aliases, so `sans-serif` resolves to a CJK
|
|
155
|
+
face.
|
|
156
|
+
|
|
157
|
+
## Making it feel better
|
|
158
|
+
|
|
159
|
+
Everything the renderer does to be cheap locally pays double here, because
|
|
160
|
+
each saved request is a saved trip across the link:
|
|
161
|
+
|
|
162
|
+
- Keep repaints **bounded**. `REACT_X11_DEBUG_PAINT=full` warns, with a
|
|
163
|
+
reason and a stack, on any silent full-window repaint —
|
|
164
|
+
[debugging.md](debugging.md). A full repaint is the difference between
|
|
165
|
+
4 kB and 40 on every frame.
|
|
166
|
+
- `REACT_X11_TRACE=summary` prints the request and byte counts for a live
|
|
167
|
+
app, so you can see what a given interaction actually costs on the wire
|
|
168
|
+
before blaming the link.
|
|
169
|
+
- `ssh -C` compresses. X11 traffic is highly compressible — repeated
|
|
170
|
+
drawing requests, text, glyph data — and on a slow link it is usually
|
|
171
|
+
worth it.
|
|
172
|
+
- Prefer one long-lived connection to many short ones: connection setup is
|
|
173
|
+
several round trips of its own, plus the font and atom warm-up.
|
|
174
|
+
- **Drags across applications are round-trip heavy**, because finding the
|
|
175
|
+
foreign window under the pointer means asking the server, per pointer
|
|
176
|
+
position. Dragging _within_ the app costs nothing extra — it never
|
|
177
|
+
touches the wire — and a drop target that answers with `e.freeze()` stops
|
|
178
|
+
the source asking while the pointer stays inside it. See
|
|
179
|
+
[drag-and-drop.md](drag-and-drop.md).
|
package/docs/scale.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Display scale
|
|
2
|
+
|
|
3
|
+
Every length in react-x11 is a **logical pixel**: a unit sized so that
|
|
4
|
+
`fontSize: 14` is comfortably readable at the distance the display is
|
|
5
|
+
actually viewed from. On the panels of the last decade one logical pixel is
|
|
6
|
+
two device pixels — or 1.5, or 1.25 — and the renderer multiplies exactly
|
|
7
|
+
once, on the way to the server. An app says nothing and gets a UI the same
|
|
8
|
+
physical size on a 2013 monitor and a retina laptop.
|
|
9
|
+
|
|
10
|
+
```jsx
|
|
11
|
+
const root = await createRoot(); // scale: 'auto' is the default
|
|
12
|
+
const root = await createRoot({ scale: 2 }); // pin it
|
|
13
|
+
REACT_X11_SCALE=1.5 node app.js // the user outranks everybody
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
import { useScale } from 'react-x11';
|
|
18
|
+
const scale = useScale(); // 1, 2, 1.5... static for the life of the root
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What is logical and what is device
|
|
22
|
+
|
|
23
|
+
**Logical — everything application code touches:**
|
|
24
|
+
|
|
25
|
+
- every style length (`width`, `padding`, `fontSize`, `borderRadius`, the
|
|
26
|
+
lengths inside `boxShadow` and gradient stops, `hitSlop`…)
|
|
27
|
+
- window geometry props (`width`, `height`, `x`, `y`, `minWidth`…) and the
|
|
28
|
+
`onResize` payload
|
|
29
|
+
- event coordinates: `ev.x`/`ev.y`/`ev.localX`/`ev.localY`, wheel
|
|
30
|
+
`deltaX`/`deltaY`, drag `screenX`/`screenY`
|
|
31
|
+
- `getClientRects()`, `measure()`, `useScreens()` rects,
|
|
32
|
+
`anchorRect`/`centerRect`/`anchorArea`
|
|
33
|
+
- the scroll surface: `scrollTo()`/`scrollBy()` arguments, `onScroll` and
|
|
34
|
+
`onViewport` payloads
|
|
35
|
+
- the theme: `DefaultTheme.fontSize` is 14 and stays 14
|
|
36
|
+
|
|
37
|
+
**Device — the renderer's internals and the deliberate escape hatches:**
|
|
38
|
+
|
|
39
|
+
- `node.abs`, yoga's computed layout, everything painted
|
|
40
|
+
- `node.scrollX`/`scrollY`/`contentWidth`/`contentHeight` read directly off
|
|
41
|
+
a ref (positions in the rendered surface; use the `onScroll` payload for
|
|
42
|
+
logical math)
|
|
43
|
+
- `<canvas onDraw>`: the browser's own canvas contract — the box arrives in
|
|
44
|
+
device pixels and the payload carries `scale`, so detail is drawn on the
|
|
45
|
+
real grid. It is deliberately not pre-scaled with `ctx.scale()`: text
|
|
46
|
+
drawn through a scaled transform moves without growing.
|
|
47
|
+
- a registered element's `paint()` and `hitTest()` (see
|
|
48
|
+
[extending.md](extending.md)): `this.style` and `this.abs` arrive already
|
|
49
|
+
device, and so are `paintDamage()`, the rects handed to `invalidate` and
|
|
50
|
+
`scrollContents` and the rects of an a11y scene; synthetic events are
|
|
51
|
+
logical, and `this.scale` is on every node for the constants that never
|
|
52
|
+
pass through a style and the event coordinate on its way to `abs`.
|
|
53
|
+
|
|
54
|
+
Fonts are the point of the whole exercise: a `fontSize: 14` at scale 2 is
|
|
55
|
+
shaped, measured and rasterized at 28px. Text on a retina panel is _sharper_,
|
|
56
|
+
not bigger-blurrier.
|
|
57
|
+
|
|
58
|
+
Fractional scales (`1.25`, `1.5`) work end to end, with the usual X11
|
|
59
|
+
caveat: a 1-logical-px hairline at 1.25x is 1 or 2 device pixels depending
|
|
60
|
+
on where it lands. Integer scales have no such seams.
|
|
61
|
+
|
|
62
|
+
"Logical" is per node rather than per window as soon as a `scale` prop is in
|
|
63
|
+
the tree: everything in the first list is divided by the scale of the node
|
|
64
|
+
it is about, which is the display's until an enclosing `scale` says
|
|
65
|
+
otherwise. See [A subtree of its own](#a-subtree-of-its-own) below.
|
|
66
|
+
|
|
67
|
+
## How `'auto'` decides
|
|
68
|
+
|
|
69
|
+
X11 has no scale protocol, so `'auto'` consults these in order and takes the
|
|
70
|
+
first that answers:
|
|
71
|
+
|
|
72
|
+
1. **Environment** — `REACT_X11_SCALE` (wins over everything, an explicit
|
|
73
|
+
`scale:` number included: it is the accessibility escape hatch), then
|
|
74
|
+
`GDK_SCALE`, then `QT_SCALE_FACTOR`.
|
|
75
|
+
2. **XSETTINGS** — `Gdk/WindowScalingFactor` when it is 2+, then `Xft/DPI`:
|
|
76
|
+
what the desktop's own settings dialog writes.
|
|
77
|
+
3. **`RESOURCE_MANAGER`** — `Xft.dpi`, the `xrdb` convention.
|
|
78
|
+
4. **RandR millimetres** — the panel's physical size against its pixel
|
|
79
|
+
size, per output, under mutter's viewing-distance model: the DPI that
|
|
80
|
+
counts as 1x is 135 under a 20″ diagonal and 110 over it, snapped to
|
|
81
|
+
quarter steps in [1, 3]. A 27″ 2560×1440 monitor is 1x, a 27″ 4K 1.5x, a
|
|
82
|
+
16″ MacBook panel 2x.
|
|
83
|
+
5. **The resolution class** — when the millimetres are absent or not
|
|
84
|
+
credible: a grid ≥3000 wide, or taller than 1600 on its short side, is
|
|
85
|
+
2x, and everything else 1x. 2560×1440 and 2560×1600 stay 1x.
|
|
86
|
+
6. **1.**
|
|
87
|
+
|
|
88
|
+
**96 is not an answer**: `Xft.dpi: 96` and `Gdk/WindowScalingFactor: 1` are
|
|
89
|
+
what daemons publish when nobody opened the dialog, so they fall through to
|
|
90
|
+
the hardware rather than pinning an unconfigured 4K laptop to 1x. And
|
|
91
|
+
**millimetres are believed only when credible**: zeroes, aspect ratios
|
|
92
|
+
posing as sizes, a physical/pixel aspect mismatch and virtual machines'
|
|
93
|
+
monitors (by EDID — QEMU, VMware, VirtualBox, Parallels, Hyper-V — and by
|
|
94
|
+
connector name, `Virtual-*`, `qxl`, …) fall through to rung 5. So do
|
|
95
|
+
`XWAYLAND*` outputs' millimetres: the compositor owns scaling there and
|
|
96
|
+
publishes its decision through rung 2.
|
|
97
|
+
|
|
98
|
+
### Servers that are not describing hardware
|
|
99
|
+
|
|
100
|
+
Rungs 4–5 read a panel, so they are skipped where the connection has none:
|
|
101
|
+
|
|
102
|
+
- **XQuartz** (the `Apple-WM` extension) hands X macOS's point space,
|
|
103
|
+
already normalised for density, and macOS scales the window onto whichever
|
|
104
|
+
panel it lands on. The answer is 1x, with the source `xquartz`.
|
|
105
|
+
- **A single synthetic output covering every monitor** — Xvfb, x11vnc,
|
|
106
|
+
TigerVNC, Xephyr and pre-RandR drivers — would read the union of two 1x
|
|
107
|
+
monitors as a retina panel, so rung 5 is skipped when Xinerama reports
|
|
108
|
+
more heads than outputs. Credible millimetres (rung 4) still count.
|
|
109
|
+
|
|
110
|
+
`REACT_X11_SCALE`, `GDK_SCALE` and a desktop's own XSETTINGS still win over
|
|
111
|
+
both. `REACT_X11_DEBUG_SCALE=1` in an app prints how the ladder resolved.
|
|
112
|
+
|
|
113
|
+
## On Wayland
|
|
114
|
+
|
|
115
|
+
A compositor has a scale protocol, and its answer is the display scale:
|
|
116
|
+
fractional-scale-v1 where it is offered (1.25, 1.5…), else
|
|
117
|
+
`wl_surface.preferred_buffer_scale`, else the integer `wl_output.scale` of
|
|
118
|
+
the output a window is on. Buffers are drawn at that density, so text is
|
|
119
|
+
as sharp at 1.5 as at 2. Two things still outrank it, as on X11:
|
|
120
|
+
|
|
121
|
+
- **`REACT_X11_SCALE`, then `createRoot({ scale })`** pin the factor,
|
|
122
|
+
absolutely: device (buffer) pixels per logical pixel. On a compositor at
|
|
123
|
+
1x, `REACT_X11_SCALE=2` is a window twice the size drawn at full
|
|
124
|
+
resolution, not a small one stretched.
|
|
125
|
+
- **The resolution class, when the compositor says 1.** When every output
|
|
126
|
+
is at 1x and the largest has no credible millimetres but a retina-class
|
|
127
|
+
grid — a VM window over a retina panel — the layout is 2x, as the X11
|
|
128
|
+
ladder answers for the same screen through Xwayland. A compositor that
|
|
129
|
+
scales any output, or credible millimetres, keep the compositor's answer.
|
|
130
|
+
|
|
131
|
+
The frame a window draws for itself follows the compositor, not the
|
|
132
|
+
layout: it is drawn at the desktop's scale, beside every other window's.
|
|
133
|
+
`REACT_X11_DEBUG_SCALE=1` prints which of these answered.
|
|
134
|
+
|
|
135
|
+
## Several monitors
|
|
136
|
+
|
|
137
|
+
Rungs 4–5 are computed for every connected output. The **root's** scale —
|
|
138
|
+
the one layout and paint use — is the primary output's (the largest, where
|
|
139
|
+
nothing is flagged primary), as GNOME does on X11: one scale for the
|
|
140
|
+
session.
|
|
141
|
+
|
|
142
|
+
The per-output answers ride on `useScreens()` — every entry carries its own
|
|
143
|
+
`scale` — so an app that places windows can do better, and a window can be
|
|
144
|
+
pinned with `createRoot({ scale })` per root. A window is **not** re-scaled
|
|
145
|
+
as it is dragged between mismatched monitors: X11 has one coordinate space
|
|
146
|
+
and no per-window scale protocol, so the scale is chosen at creation and
|
|
147
|
+
static after.
|
|
148
|
+
|
|
149
|
+
It is static for the session too: a mid-session change of `Xft.dpi` or a
|
|
150
|
+
monitor swap does not re-scale running windows. Restart the app.
|
|
151
|
+
|
|
152
|
+
## A subtree of its own
|
|
153
|
+
|
|
154
|
+
The display scale is one number for the whole window. `scale` on an element
|
|
155
|
+
is a second factor for one **subtree** of it:
|
|
156
|
+
|
|
157
|
+
```jsx
|
|
158
|
+
<box scale={zoom} style={{ width: w / zoom, height: h / zoom }}>
|
|
159
|
+
<NodeBody /> // written in plain logical pixels, unaware of the zoom
|
|
160
|
+
</box>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The semantics are CSS's `zoom`, not a transform:
|
|
164
|
+
|
|
165
|
+
- A node's **effective scale** is its parent's times its own `scale` prop,
|
|
166
|
+
so nested props multiply and the display scale is the floor they multiply.
|
|
167
|
+
`node.scale` is that number, and everything the display scale reaches —
|
|
168
|
+
styles, the caret and scrollbars, shadows and gradients, a registered
|
|
169
|
+
element's own `paint()` — follows it.
|
|
170
|
+
- **The node's own style scales too.** `scale={2}` on a box with
|
|
171
|
+
`padding: 8` gets 16 device pixels of padding, not 8 with bigger children
|
|
172
|
+
inside it. That is what makes the caller's job the one-liner above: divide
|
|
173
|
+
the size you want the box to occupy by the zoom, and write everything
|
|
174
|
+
inside it at 1x.
|
|
175
|
+
- **Text is shaped at the size it is drawn at.** A `fontSize: 14` inside
|
|
176
|
+
`scale={2}` is shaped, measured and rasterized at 28 — the same thing the
|
|
177
|
+
display scale does, for the same reason. A zoomed-in graph is not a
|
|
178
|
+
stretched screenshot of a zoomed-out one.
|
|
179
|
+
- **Yoga lays out the scaled numbers** like any others, so a zoomed subtree
|
|
180
|
+
wraps, flexes and measures its content floors at the size it is drawn.
|
|
181
|
+
|
|
182
|
+
What it is for: an element that owns a viewport — a graph pane, a canvas, a
|
|
183
|
+
map — and mounts application content inside it. The pane draws its own
|
|
184
|
+
world, and the React subtree beside it follows the same zoom, laid out,
|
|
185
|
+
drawn and hit at the zoomed size.
|
|
186
|
+
|
|
187
|
+
### What a boundary costs
|
|
188
|
+
|
|
189
|
+
**Events are in the target's unit.** `ev.x`, `ev.y`, `localX`/`localY` and
|
|
190
|
+
`getClientRects()` on a node inside a zoomed subtree are all divided by
|
|
191
|
+
_that node's_ scale, so they agree with each other and with the styles it
|
|
192
|
+
was written in — a handler inside the subtree needs to know nothing. The
|
|
193
|
+
trade-off is CSS `zoom`'s: a handler on an **unzoomed ancestor** reads a
|
|
194
|
+
coordinate in the zoomed descendant's unit, because the target is what
|
|
195
|
+
decides. When a pane needs its own coordinates for a hit that landed inside
|
|
196
|
+
the content it hosts, `ev.nativeEvent` is the way back — device pixels of
|
|
197
|
+
the window, which `/ node.scale` converts to whichever unit is wanted.
|
|
198
|
+
`screenX`/`screenY` are unaffected: they are desktop coordinates, and the
|
|
199
|
+
desktop has one scale.
|
|
200
|
+
|
|
201
|
+
A wheel notch scrolls `48` of the target's logical pixels, so content under
|
|
202
|
+
the pointer travels the distance the zoom says it should.
|
|
203
|
+
|
|
204
|
+
**A real X window is its own root.** `<window>` and `<popup>` take no
|
|
205
|
+
`scale` prop and ignore an enclosing one: their geometry is the server's, a
|
|
206
|
+
window manager sees no zoom, and a menu opened from a zoomed card should
|
|
207
|
+
come up at the size the rest of the app's menus are. `<glarea>` and
|
|
208
|
+
`<foreign>` do inherit — their boxes are laid out by the parent like any
|
|
209
|
+
other child's.
|
|
210
|
+
|
|
211
|
+
**Quantise a gesture-driven zoom.** Every distinct factor is a distinct
|
|
212
|
+
font size to shape and cache, so a wheel handler feeding the raw accumulator
|
|
213
|
+
into `scale` makes a new set of glyphs per frame. Round to a step — 5%, or
|
|
214
|
+
a ladder of preset zooms — and the cache does its job.
|
|
215
|
+
|
|
216
|
+
## In tests
|
|
217
|
+
|
|
218
|
+
The headless harnesses resolve to exactly 1 — a mock app has no display to
|
|
219
|
+
ask — so a geometry assertion means its numbers literally.
|
|
220
|
+
`createRoot({ app, scale: 2 })` opts a test in, and so does
|
|
221
|
+
`renderX11(element, { scale: 2 })` from `react-x11/test`.
|
|
222
|
+
|
|
223
|
+
Design notes — the ladder's rationale, prior art, and why lengths are
|
|
224
|
+
multiplied rather than the context transformed:
|
|
225
|
+
[design/scale.md](../design/scale.md).
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Security model
|
|
2
|
+
|
|
3
|
+
Read this before you decide what to put in a react-x11 window.
|
|
4
|
+
|
|
5
|
+
## X11 has no isolation between clients
|
|
6
|
+
|
|
7
|
+
Any program connected to an X display can read any other program's window
|
|
8
|
+
contents, grab the keyboard, and synthesize or record input. Not through a
|
|
9
|
+
bug — that is the 1987 design. X11 predates the idea that programs on one
|
|
10
|
+
machine might not trust each other, and the protocol's power was the point.
|
|
11
|
+
|
|
12
|
+
It is not theoretical, and it is not hard. node-x11 ships bindings for both
|
|
13
|
+
extensions that make it a five-line program on a shared display:
|
|
14
|
+
|
|
15
|
+
- **XTEST** (`x11/lib/ext/xtest.js`) — `FakeInput` synthesizes key presses
|
|
16
|
+
and pointer events indistinguishable from real ones.
|
|
17
|
+
- **RECORD** (`x11/lib/ext/record.js`) — a keylogger, by design; it exists
|
|
18
|
+
so that macro recorders can work.
|
|
19
|
+
|
|
20
|
+
Plus `GetImage` on another client's window, which needs no extension at all
|
|
21
|
+
and is what this project's own [framed screenshots](../AGENTS.md) use.
|
|
22
|
+
|
|
23
|
+
Two consequences, both practical:
|
|
24
|
+
|
|
25
|
+
1. **Do not run untrusted programs on a display you use.** A malicious npm
|
|
26
|
+
postinstall script that reaches `$DISPLAY` owns your session — your
|
|
27
|
+
browser's rendered pages, your password manager's window, your
|
|
28
|
+
keystrokes.
|
|
29
|
+
2. **Do not treat a react-x11 window as a confidential surface.** If a
|
|
30
|
+
secret must not be readable by other software on that display, it does
|
|
31
|
+
not belong in an X11 window. This applies to every X11 toolkit equally;
|
|
32
|
+
react-x11 has no way to change it and does not pretend to.
|
|
33
|
+
|
|
34
|
+
Under Wayland this is different — a Wayland client cannot read its
|
|
35
|
+
neighbours — but react-x11 running under Xwayland is inside the X11 sandbox
|
|
36
|
+
with every other X client, so it shares theirs and not the compositor's. The
|
|
37
|
+
opt-in Wayland backend (`backend: 'wayland'`,
|
|
38
|
+
[wayland-backend.md](wayland-backend.md)) is a native Wayland client instead,
|
|
39
|
+
and is isolated the way any other one is. The Cocoa and win32 backends
|
|
40
|
+
connect to no X display at all, so what isolation they have is the
|
|
41
|
+
platform's, not X11's.
|
|
42
|
+
|
|
43
|
+
## `$XAUTHORITY` is a password
|
|
44
|
+
|
|
45
|
+
Access to an X display is a **bearer token**: the MIT-MAGIC-COOKIE-1 cookie
|
|
46
|
+
in `~/.Xauthority` (or wherever `$XAUTHORITY` points). node-x11 sends it on
|
|
47
|
+
connect (`x11/lib/auth.js`). Whoever holds the cookie has the display, with
|
|
48
|
+
everything in the section above.
|
|
49
|
+
|
|
50
|
+
So:
|
|
51
|
+
|
|
52
|
+
- **Never bake it into a container image.** `COPY . /app` with an
|
|
53
|
+
`.Xauthority` in the build context ships your display credentials to
|
|
54
|
+
whoever pulls the image.
|
|
55
|
+
- **Never log it, and never put it in a bug report.** Nor
|
|
56
|
+
`xauth list` output, nor a strace of the connect handshake.
|
|
57
|
+
- **Do not mount `~/.Xauthority` into a container** you would not trust with
|
|
58
|
+
your desktop. Mounting `/tmp/.X11-unix` has the same effect.
|
|
59
|
+
- Cookies are per-display and long-lived. Rotating one means
|
|
60
|
+
`xauth remove` / `xauth generate`, not restarting the app.
|
|
61
|
+
|
|
62
|
+
`ssh -X` sidesteps all of this: it creates a **separate, per-session,
|
|
63
|
+
expiring** cookie on the remote side, which is worth far less if it leaks.
|
|
64
|
+
That is a security argument for `ssh -X` beyond the encryption.
|
|
65
|
+
|
|
66
|
+
## Prefer `ssh -X` over `ssh -Y`
|
|
67
|
+
|
|
68
|
+
`-X` runs the app as an untrusted client: it cannot read other windows,
|
|
69
|
+
cannot grab the keyboard, cannot record input. `-Y` turns those restrictions
|
|
70
|
+
off and hands the remote host your whole session.
|
|
71
|
+
|
|
72
|
+
**react-x11 should work under `-X`.** It draws into windows it created,
|
|
73
|
+
which is exactly what untrusted mode allows. **If it does not, that is a bug
|
|
74
|
+
worth filing** rather than a reason to switch to `-Y`. That is a deliberate
|
|
75
|
+
position: most toolkits' documentation says "use `-Y` if `-X` misbehaves",
|
|
76
|
+
which quietly trades the entire isolation model for a workaround.
|
|
77
|
+
|
|
78
|
+
The one thing untrusted mode costs you is a cookie that expires — see
|
|
79
|
+
[remote.md](remote.md#the-failure-that-looks-like-a-crash), because the
|
|
80
|
+
symptom looks like a crash rather than a permission problem.
|
|
81
|
+
|
|
82
|
+
## Never `xhost +`
|
|
83
|
+
|
|
84
|
+
`xhost +` disables access control for every host on the network. `xhost
|
|
85
|
+
+local:` does it for every user on the machine, including the ones you did
|
|
86
|
+
not have in mind. Both are common advice for "X11 in Docker" and both mean
|
|
87
|
+
"anything that can reach my display now owns my session".
|
|
88
|
+
|
|
89
|
+
The right tool is `xauth`: extract the cookie for one display and give it to
|
|
90
|
+
one consumer.
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# hand a container the cookie for :0, and nothing else
|
|
94
|
+
xauth nextract - "$DISPLAY" | docker exec -i box xauth nmerge -
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`ssh -X` is better still, because the cookie it hands over is not the one
|
|
98
|
+
that opens your desktop.
|
|
99
|
+
|
|
100
|
+
## What react-x11 does and does not do
|
|
101
|
+
|
|
102
|
+
**Does:**
|
|
103
|
+
|
|
104
|
+
- Sends the cookie ntk/node-x11 read from `$XAUTHORITY` — the standard
|
|
105
|
+
handshake, nothing custom.
|
|
106
|
+
- Contains a throwing event handler rather than letting it take the process
|
|
107
|
+
([events.md](events.md#when-a-handler-throws)); a bad handler is a bug, not
|
|
108
|
+
a way to wedge the app.
|
|
109
|
+
- Bounds what it draws to the damaged region, which is a performance
|
|
110
|
+
property but also means less of your window crosses the wire per frame.
|
|
111
|
+
|
|
112
|
+
**Does not:**
|
|
113
|
+
|
|
114
|
+
- Defend against a hostile X server. The server you connect to sees
|
|
115
|
+
everything you draw, everything you type into your own windows, and holds
|
|
116
|
+
your clipboard. Connecting to an X display is trusting it completely —
|
|
117
|
+
there is no protocol-level way to do otherwise, and no library can add
|
|
118
|
+
one. `DISPLAY=evil.example:0` is equivalent to handing over the session.
|
|
119
|
+
- Sanitize content. `<svg>` renders the markup you give it through ntk's
|
|
120
|
+
SvgView, and a document component renders the source you hand it —
|
|
121
|
+
untrusted markup should be treated the way you would treat it in a
|
|
122
|
+
browser. Anything that fetches a remote resource on a document's behalf
|
|
123
|
+
is yours to police.
|
|
124
|
+
- Isolate one window from another within an app. Every node in a root shares
|
|
125
|
+
one connection and one process; `trapFocus` is a focus policy, not a
|
|
126
|
+
security boundary, and neither is a modal `Dialog`
|
|
127
|
+
([components.md](components.md#dialog)).
|
|
128
|
+
- Encrypt anything. That is `ssh`'s job.
|
|
129
|
+
|
|
130
|
+
## Reporting a vulnerability
|
|
131
|
+
|
|
132
|
+
See [SECURITY.md](../SECURITY.md).
|