react-x11 2.52.0 → 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 (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
package/docs/scale.md CHANGED
@@ -42,10 +42,8 @@ const scale = useScale(); // 1, 2, 1.5... static for the life of the root
42
42
  logical math)
43
43
  - `<canvas onDraw>`: the browser's own canvas contract — the box arrives in
44
44
  device pixels and the payload carries `scale`, so detail is drawn on the
45
- real grid. Deliberately **not** a `ctx.scale()`: ntk positions glyphs
46
- through the transform but sizes them from the font, so a scaled transform
47
- would move an app's `fillText` without growing it — and it would knock
48
- every fill off ntk's server-side fast paths besides.
45
+ real grid. It is deliberately not pre-scaled with `ctx.scale()`: text
46
+ drawn through a scaled transform moves without growing.
49
47
  - a registered element's `paint()` and `hitTest()` (see
50
48
  [extending.md](extending.md)): `this.style` and `this.abs` arrive already
51
49
  device, and so are `paintDamage()`, the rects handed to `invalidate` and
@@ -55,108 +53,62 @@ const scale = useScale(); // 1, 2, 1.5... static for the life of the root
55
53
 
56
54
  Fonts are the point of the whole exercise: a `fontSize: 14` at scale 2 is
57
55
  shaped, measured and rasterized at 28px. Text on a retina panel is _sharper_,
58
- not bigger-blurrier — the same reason every native toolkit renders this way
59
- instead of transform-scaling a 1x picture.
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.
60
61
 
61
62
  "Logical" is per node rather than per window as soon as a `scale` prop is in
62
63
  the tree: everything in the first list is divided by the scale of the node
63
64
  it is about, which is the display's until an enclosing `scale` says
64
- otherwise. See "A subtree of its own" below.
65
+ otherwise. See [A subtree of its own](#a-subtree-of-its-own) below.
65
66
 
66
67
  ## How `'auto'` decides
67
68
 
68
- X11 never grew a scale protocol, so the answer is scattered across four
69
- generations of convention. The ladder consults each rung only when the ones
70
- above said nothing, ordered by "who is closest to a human having decided":
71
-
72
- 1. **Environment** — `REACT_X11_SCALE` (ours, wins over everything including
73
- an explicit `scale:` number — it is the accessibility escape hatch), then
74
- `GDK_SCALE`, then `QT_SCALE_FACTOR`. The user already told their other
75
- toolkits; an app of ours on the same desktop should agree.
76
- 2. **XSETTINGS** — `Gdk/WindowScalingFactor` when it is 2+, then `Xft/DPI`
77
- (in 1024ths of a dpi on the wire; a daemon writing plain dpi is read
78
- right too). This is what the desktop's own settings dialog writes and
79
- what every GTK app on the screen is already obeying.
80
- 3. **`RESOURCE_MANAGER`** — `Xft.dpi`, the `xrdb` convention winit,
81
- Chromium and every terminal emulator read.
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.
82
78
  4. **RandR millimetres** — the panel's physical size against its pixel
83
- size, run per output, under mutter's viewing-distance model: the DPI
84
- that counts as "1x" is 135 under a 20″ diagonal (a panel in your lap at
85
- ~50cm) and 110 over it (across a desk). Snapped to quarter steps in
86
- [1, 3]. For calibration: a 27″ 2560×1440 desk monitor computes 109dpi →
87
- 1x; a 27″ 4K computes 163dpi → 1.5x; a 16″ MacBook panel computes
88
- 255dpi → 2x, matching macOS.
89
- 5. **The resolution class** — when the millimetres are absent or caught
90
- lying, the pixel grid itself is the last signal: nothing ships as a 1x
91
- panel with a grid ≥3000 wide or taller than 1600 on its short side, so
92
- those are called 2x and everything else 1x. That takes in the 2560×1664
93
- and 2880×1800 lids, and a VM window over any retina panel (a UTM window
94
- on a 14″ MacBook is 2488×1668). Only the confident call is made without
95
- physical data — 2560×1440 and 2560×1600 stay 1x on purpose, because
96
- they are the commonest 1x desk monitors there are and only millimetres
97
- could tell them from a 13″ retina lid.
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.
98
86
  6. **1.**
99
87
 
100
- Two readings deserve their asterisks. **96 is not an answer**: `Xft.dpi: 96`
101
- and `Gdk/WindowScalingFactor: 1` are what daemons publish when nobody ever
102
- opened the dialog (xfsettingsd writes both unconditionally), so they fall
103
- through to the hardware instead of pinning an unconfigured 4K laptop to
104
- microscopic. And **millimetres are audited before they are believed**
105
- (`classifyMm`): zeroes, aspect-ratios-as-sizes (16×9 "millimetres"),
106
- physical/pixel aspect disagreement, and — the one that motivated all of
107
- this — virtual machines, detected by EDID vendor and model (`RHT` / "QEMU
108
- Monitor", VMware, VirtualBox, Parallels, Hyper-V) and by connector name
109
- (`Virtual-*`, `qxl`, …). `XWAYLAND*` outputs skip the hardware rung too:
110
- the compositor owns scaling there and publishes its decision through rung
111
- 2, in both of its Xwayland modes.
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.
112
97
 
113
98
  ### Servers that are not describing hardware
114
99
 
115
- Rungs 4–5 read a panel, so they are skipped where the connection has no
116
- panel to describe. Two cases, both of them servers people really run:
117
-
118
- - **XQuartz**, detected by the `Apple-WM` extension. macOS composes the
119
- desktop in _points_ and hands X the point space, already normalised for
120
- density: a 16″ retina lid arrives as 1728×1080, a 1x monitor beside it as
121
- 2560×1403, and the window server scales what it is given onto whichever
122
- panel the window lands on. Inferring a factor here doubles a size macOS
123
- is about to double again, so the ladder answers 1x with the source
124
- `xquartz` — the same exemption `XWAYLAND` gets, for the same reason.
125
- - **A single synthetic output covering every monitor.** XQuartz, Xvfb,
126
- x11vnc and TigerVNC, Xephyr and pre-RandR drivers answer the output walk
127
- with one output — usually named `default`, always without millimetres —
128
- whose CRTC is the _union_ of the desktop. Two 2560×1440 monitors union to
129
- 5120×1440, which rung 5 reads as a retina panel; the machine this was
130
- found on unioned a retina lid and two 1x monitors into 5120×2520 and drew
131
- every widget at double size. Xinerama reports those heads separately, so
132
- more heads than outputs retires rung 5 (`isUnionOutput`). Rung 4 survives
133
- it — credible millimetres describe real glass however the pixels were
134
- divided up, which is what an `xrandr --setmonitor` split of one ultrawide
135
- is.
136
-
137
- Both guards sit _below_ the configured rungs: `REACT_X11_SCALE`,
138
- `GDK_SCALE` and a desktop's own XSETTINGS still win, because a person who
139
- typed a factor outranks the platform.
140
-
141
- ### The machine this was built against
142
-
143
- A 16″ MacBook panel handed 1:1 to a UTM Linux guest (XFCE, X11) defeats
144
- rungs 1–4 simultaneously, which is why the ladder has five:
100
+ Rungs 4–5 read a panel, so they are skipped where the connection has none:
145
101
 
146
- ```
147
- core screen 3456×2168 px, "914×573mm" → synthesized to exactly 96dpi
148
- XSETTINGS Gdk/WindowScalingFactor 1, Xft/DPI 98304 (= 96·1024)
149
- RESOURCE_MANAGER Xft.dpi: 96 → the never-configured default
150
- RandR Virtual-1 "870×550mm" → a 40″ panel that is 16″
151
- EDID vendor RHT, model "QEMU Monitor" → the lie, signed
152
- verdict 2x via the resolution class → correct
153
- ```
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.
154
109
 
155
- Every millimetre in that transcript is invented — QEMU fabricates a size
156
- that makes the maths land on ~100dpi — and every configured source is an
157
- untouched default. Run `node scripts/scale-probe.mjs` against any display
158
- to see the same table for it, or `REACT_X11_DEBUG_SCALE=1` in an app to
159
- watch the ladder resolve.
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.
160
112
 
161
113
  ## On Wayland
162
114
 
@@ -170,16 +122,11 @@ as sharp at 1.5 as at 2. Two things still outrank it, as on X11:
170
122
  absolutely: device (buffer) pixels per logical pixel. On a compositor at
171
123
  1x, `REACT_X11_SCALE=2` is a window twice the size drawn at full
172
124
  resolution, not a small one stretched.
173
- - **The resolution class, when the compositor says 1.** A VM window over a
174
- retina panel is the case: QEMU's EDID describes the guest's screen as a
175
- ~100dpi monitor, mutter believes it and runs at 100%, and every app laid
176
- out at the compositor's word comes up half the size it is on the host.
177
- When every output is at 1x and the largest has no credible millimetres
178
- (`classifyMm`) but a retina-class grid, the layout is 2x — what the X11
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
179
128
  ladder answers for the same screen through Xwayland. A compositor that
180
- scales any output has decided for itself (133% on that same VM is
181
- someone's choice), and credible millimetres were the compositor's own
182
- evidence, so both keep their answer.
129
+ scales any output, or credible millimetres, keep the compositor's answer.
183
130
 
184
131
  The frame a window draws for itself follows the compositor, not the
185
132
  layout: it is drawn at the desktop's scale, beside every other window's.
@@ -187,24 +134,20 @@ layout: it is drawn at the desktop's scale, beside every other window's.
187
134
 
188
135
  ## Several monitors
189
136
 
190
- Rungs 4–5 are computed for every connected output, because a desktop with a
191
- retina lid and an office monitor genuinely has two answers. The **root's**
192
- scale — the one layout and paint use — is the primary output's (the largest,
193
- where nothing is flagged primary), matching what GNOME does on X11: one
194
- scale for the session, chosen for the display you called primary.
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.
195
141
 
196
142
  The per-output answers ride on `useScreens()` — every entry carries its own
197
143
  `scale` — so an app that places windows can do better, and a window can be
198
- pinned with `createRoot({ scale })` per root. What this deliberately does
199
- not do is re-scale a window as it is dragged between mismatched monitors:
200
- X11 has one coordinate space and no per-window scale protocol, so that move
201
- is a resize the window manager fights mid-drag; Qt is the one toolkit that
202
- tries it, and "chosen at creation, static after" is the behaviour of
203
- everything else on this window system.
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.
204
148
 
205
- Static-at-startup is the other honest limitation: a mid-session change of
206
- `Xft.dpi` or a monitor swap does not re-scale running windows. Rare enough
207
- that GTK3 apps mostly share it; restart the app.
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.
208
151
 
209
152
  ## A subtree of its own
210
153
 
@@ -221,10 +164,9 @@ The semantics are CSS's `zoom`, not a transform:
221
164
 
222
165
  - A node's **effective scale** is its parent's times its own `scale` prop,
223
166
  so nested props multiply and the display scale is the floor they multiply.
224
- `node.scale` is that number, and every path that already asked for it —
225
- `scaleResolvedStyle` at the end of the style funnel, the caret and
226
- scrollbar constants, shadows and gradients, a registered element's own
227
- `paint()` — follows with no second mechanism.
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.
228
170
  - **The node's own style scales too.** `scale={2}` on a box with
229
171
  `padding: 8` gets 16 device pixels of padding, not 8 with bigger children
230
172
  inside it. That is what makes the caller's job the one-liner above: divide
@@ -239,11 +181,8 @@ The semantics are CSS's `zoom`, not a transform:
239
181
 
240
182
  What it is for: an element that owns a viewport — a graph pane, a canvas, a
241
183
  map — and mounts application content inside it. The pane draws its own
242
- world, and the React subtree beside it has to follow the same zoom;
243
- `ctx.scale()` cannot do that here (see "Why multiply values instead of
244
- transforming the context" below) and a layer transform on the Cocoa
245
- presenter would draw a zoomed body whose hit testing still lived at the
246
- unzoomed rects.
184
+ world, and the React subtree beside it follows the same zoom, laid out,
185
+ drawn and hit at the zoomed size.
247
186
 
248
187
  ### What a boundary costs
249
188
 
@@ -274,66 +213,13 @@ font size to shape and cache, so a wheel handler feeding the raw accumulator
274
213
  into `scale` makes a new set of glyphs per frame. Round to a step — 5%, or
275
214
  a ladder of preset zooms — and the cache does its job.
276
215
 
277
- ## Prior art, and where this sits
278
-
279
- | Toolkit | X11 sources, in order | Notes |
280
- | -------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
281
- | GTK 3/4 | `GDK_SCALE` env; XSETTINGS `Gdk/WindowScalingFactor` | integer only; fonts via `Xft/DPI` |
282
- | Qt 5/6 | `QT_SCALE_FACTOR`, `QT_SCREEN_SCALE_FACTORS`; per-screen logical DPI (`Xft.dpi`), physical DPI from RandR | the only per-screen attempt on X11 |
283
- | winit | `WINIT_X11_SCALE_FACTOR`; `Xft.dpi`; RandR mm | mm path famously burned by 1×1mm and VM EDIDs ([winit#1983](https://github.com/rust-windowing/winit/issues/1983)) |
284
- | Chromium/Electron | `Xft.dpi` via XSETTINGS then xrdb; `GDK_SCALE` | never trusts RandR mm |
285
- | SDL 2/3 | `Xft.dpi` only | 1x on every unconfigured desktop |
286
- | mutter (the config UI's default) | per-monitor mm under the 135/110-dpi viewing-distance model | the model rung 4 adopts, constants and all |
287
-
288
- The composition is the contribution: desktop configuration first (rungs
289
- 1–3, which is where Chromium/SDL stop — correct on configured desktops,
290
- tiny on unconfigured HiDPI ones), mutter's perceptual model where the
291
- hardware is honest (which is where winit's mm math stops — right until an
292
- EDID lies), and the resolution class where it is not (which nobody else
293
- has, and which is the only rung that survives a VM).
294
-
295
- ## Design notes
296
-
297
- **Why multiply values instead of transforming the context.** Two hard
298
- reasons, both ntk's. Its 2d context batches solid fills into server-side
299
- `Render.FillRectangles` and takes similar fast paths for strokes, rounded
300
- rects and glyph runs — all gated on an identity transform, so a frame-wide
301
- `ctx.scale(2,2)` would rasterize every box in the UI as polygons. And its
302
- text model sizes glyphs from the font while only _positioning_ them through
303
- the transform, so scaled-transform text comes out moved but not grown.
304
- Multiplying at the style funnel instead means paint code, damage rects, the
305
- scroll blit and the paint cache all keep working in integer device pixels
306
- with the transforms they always had. (`paintcache.js` even guards against
307
- the other choice: a non-identity CTM disables it entirely.)
308
-
309
- **Where the multiply lives.** One place for styles —
310
- `scaleResolvedStyle()`, at the end of the style funnel in `_syncStyle`, so
311
- state blocks, size queries and the flex shorthand all still think in the
312
- logical pixels the app wrote. One place for the theme's font size — the
313
- root branch of `inheritedTextStyle`, because descendants inherit an
314
- already-resolved size and any second multiply would compound. One place for
315
- window geometry (`scaleWindowGeometry`), one per string-parsed decoration
316
- (`shadowSpecs`/`gradientSpec`, whose parses are memoized on the raw string
317
- so the scale rides into the memo key), and division at each app-facing
318
- egress (`SyntheticEvent`, `getClientRects`, `onScroll`, `useScreens`…).
319
- `yoga` needs no `pointScaleFactor` games: layout runs directly in device
320
- pixels and its ordinary whole-pixel rounding is what keeps a 1-logical-px
321
- border crisp at 1.5x.
322
-
323
- **Fractional scales** work end to end (`1.25`, `1.5`, quarter steps from
324
- the ladder), with the usual X11 caveat: a 1-logical-px hairline at 1.25x is
325
- 1 or 2 device pixels depending on where it lands. Integer scales have no
326
- such seams.
327
-
328
- **What tests see.** The headless harnesses resolve to exactly 1 — a mock
329
- app has no display to ask — so every geometry assertion in the suite means
330
- its numbers literally. `createRoot({ app, scale: 2 })` opts a test in, and
331
- so does `renderX11(element, { scale: 2 })` from `react-x11/test`;
332
- `test/scale-render.test.js` is the boundary contract pinned as tests.
333
- `setScaleForTests()` (from `src/scale.js`) pins a factor without a
334
- connection.
335
-
336
- **Known costs.** The paint cache's per-item budget is device pixels, so at
337
- 2x a widget occupies 4× the budget it did — icons and checkboxes still fit
338
- comfortably. Damage rects, protocol traffic and backing stores scale the
339
- same way any HiDPI surface does.
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).