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.
Files changed (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. 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).
@@ -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).