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
@@ -1,854 +0,0 @@
1
- # Windowed regions: when X11 subwindows pay, and what API to give them
2
-
3
- _Research document, 2026-08-01. Measured against the `perf/scroll-blit` checkout (nodes.js @
4
- 4219 lines), ntk 4.2.0 installed. All measurements ran on one machine (Apple M1 Pro, macOS 15.2,
5
- node v26): live XQuartz 2.8.6 (X.Org 21.1.23, custom build), Xvfb 21.1.23 (stock Xorg
6
- dix/mi/fb), and node-x11's in-process pure-JS X server via the repo's own bench harness.
7
- Benchmark scripts and raw research notes are preserved out-of-tree (see §10); file:line
8
- references are to the checkout above and will drift with the code._
9
-
10
- ---
11
-
12
- ## 0. TL;DR
13
-
14
- Today every control is drawn into one surface per `<window>`/`<popup>`; hover on 1 of 1000
15
- controls means `PointerMotion` selected on the whole window plus an O(N) JS hit test per motion
16
- frame. The research question: when should a control or region get a real X subwindow instead,
17
- and how automatic should that be?
18
-
19
- **Findings, compressed:**
20
-
21
- 1. **Subwindows are cheap as objects and expensive as a lifestyle.** A window costs ~290 B server
22
- heap, ~5 µs to create, 8 B to destroy recursively. But per-child map/move storms are O(siblings)
23
- each (654 ms to nudge 5000 mapped children on XQuartz), every toolkit that shipped
24
- window-per-widget abandoned it, and core X cannot alpha-blend a child over its parent — no
25
- translucency, no antialiased rounded corners, no drawn overlays above a windowed region.
26
- **Window-per-control is dead on arrival; nothing here re-litigates that.**
27
-
28
- 2. **The measured hover cost is client-side JS, not the wire — and it is fixable client-side.**
29
- A motion frame at 1000 controls costs ~27 µs and ~8–11 KB of allocation, ~94% of it in
30
- `hitTest`'s per-node `paintOrder()` churn and unpruned descent, not in the hover machinery.
31
- GTK4 hit-tests every motion client-side and considers it fine; Flutter does it at 120 Hz on
32
- phones — the fix is caching + pruning (see §5.1), not protocol surgery. Subwindow event masks
33
- would win the _wire_ (zero motion traffic, server-side Enter/Leave picking at ~3 ns per mapped
34
- sibling), which matters for remote X and battery, but they are not needed to fix the CPU cost.
35
-
36
- 3. **The one region where a subwindow structurally wins is the scroll pane.** Moving a child window
37
- is a server-side blit with Expose only for the revealed strip — verified on XQuartz and Xorg —
38
- i.e. exactly what the scroll-blit branch hand-builds, minus all ~12 purity gates, the per-frame
39
- O(N) safety walk, the scrollbar-repair choreography, and the per-notch full-tree
40
- relayout+absolutize. A `viewport window + content window` scroll pane turns a scroll into one
41
- 20-byte ConfigureWindow — and on Composite servers (**the deployment majority**: Xorg+
42
- compositor, XWayland), a `backing-store: WhenMapped` content pane scrolls with **zero Expose**:
43
- the server restores the revealed strip from its own backing. XQuartz (minority, no Composite)
44
- falls back to Expose-strip repaints served from ntk's pixmap. Constraints: positions are INT16
45
- (±32767 → re-anchoring for long content), backing memory for the content pane, and drawn rows
46
- only (windowed rows inside the pane would pay O(children) clip revalidation per scroll frame).
47
-
48
- 4. **API answer from history and from our numbers: automatic-off, explicit-in, at region
49
- granularity.** Every surviving toolkit converged on: native windows for toplevels + popups
50
- (react-x11 already does this), plus a small number of explicitly-promoted surfaces where the
51
- server gives something the drawn path cannot (GL/video/foreign — `<glarea>` already exists),
52
- plus framework-inserted promotion at _known_ hotspots (Flutter's repaint boundaries around
53
- scrollables). Fully automatic promotion exists only in browsers and needed a decade of
54
- squashing heuristics. The concrete proposal is in §8: keep `<box windowed>` out of the public
55
- API; instead (a) fix the client-side hit test, (b) make the scroll pane internally windowed
56
- behind a feature gate, (c) keep real windows implicit-by-capability (glarea, future
57
- `<foreign>`), and (d) optionally spike InputOnly hit-region windows for per-region cursors and
58
- hover-during-scroll correctness — the only subwindow variant with zero rendering downside.
59
- **§8.1 turns this into an issue-ready, dependency-ordered action plan** (code, API, and docs
60
- items with acceptance criteria).
61
-
62
- 5. **Gravity is a latency mask, not a layout engine.** `winGravity` can pin corner/edge-anchored
63
- children server-side during a parent resize (it works on XQuartz — quartz-wm itself relies on
64
- it), but it expresses only `x = a + b·W, b ∈ {0, ½, 1}` and never resizes anything; flex-grow,
65
- stretch (yoga's default), percentages and space-between all need the client anyway. Useful only
66
- to hide one frame of resize latency on anchored panes; not a reason to add windows.
67
-
68
- ### 0.1 Cross-check against the project goals
69
-
70
- The goal hierarchy this document is graded against: **(main) perceived performance** — minimum
71
- input→screen delay, achieved by prepping while idle, updating as much as possible instantly, and
72
- catching up afterwards; **(note) visual stability** — no stutter, no unexpected jumps, no
73
- queued-event storms; **(secondary) client CPU first, server CPU second.**
74
-
75
- | item | input→photon latency | stability | client CPU | server CPU |
76
- | -------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------------ |
77
- | A. hit-test caching/pruning | small direct win; big _jitter_ win (less GC) | **GC-pause stutter shrinks** (8–11 KB/event → ~1 KB) | the top measured win | — |
78
- | B. windowed scroll pane | **the headline win**: scroll applied at server speed, decoupled from React | server blit is atomic; re-anchor seams and hover churn must be engineered (below) | deletes per-notch relayout + gate walks | small (blit + O(pane siblings) clip) |
79
- | C. capability windows (glarea/foreign) | n/a (capability, not perf) | airspace rules as documented | — | — |
80
- | D. InputOnly hit regions | **cursor feedback becomes server-side-instant**, immune to client busyness | Enter/Leave uncoalesced → needs frame debounce | motion stream off when idle | ~µs picking |
81
- | gravity (§5.5) | **upgraded under this lens**: "latency mask" _is_ the main goal during resize gestures | `winGravity: Unmap` prevents stale-position jumps | — | — |
82
- | window-per-control | still dead: storms _add_ latency and jank | storms are anti-stability | — | O(siblings) each |
83
-
84
- What the lens changes, specifically:
85
-
86
- - **B's real rationale is latency, not request counts.** Today a wheel notch is: discrete
87
- ButtonPress → `scrollBy` → `invalidate(true)` → _wait for the paced frame_ (up to 16 ms +
88
- fence) → full-tree relayout + absolutize → repaint/blit → present. With a windowed pane the
89
- ConfigureWindow can be written **synchronously inside the wheel handler, before React runs
90
- anything** — the screen moves at server speed, and layout/virtualization catch up on the
91
- following frames. Scroll responsiveness decouples from render cost entirely: even mid-GC or
92
- mid-heavy-render, queued wheel events still turn into 20-byte moves the server applies
93
- smoothly, instead of accumulating into one visible catch-up jump. This is the
94
- "update instantly, then catch up" pattern made structural.
95
- - **Idle prep has a concrete new lever: overdraw prepainting.** A content pane sized
96
- viewport+overdraw can have its off-screen band rasterized into backing _while idle_ — then the
97
- next N notches reveal already-painted pixels with zero client work at input time (server
98
- restores from backing on Composite targets). Today's window-sized backing cannot prepaint
99
- beyond the viewport at all. Idle time becomes scroll headroom; re-anchor work also belongs in
100
- idle, never on the input path.
101
- - **Stability items promote from "risks" to requirements.** (1) One shared frame clock /
102
- atomic present per top-level is a _stability_ requirement, not an optimization — two regions
103
- presenting independently in one gesture is visible tearing (Present is the long-term protocol
104
- answer). (2) Enter/Leave arrive uncoalesced → hover restyles need a frame-level debounce or
105
- crossing storms cause per-crossing paints. (3) Re-anchor seams must be invisible: prepaint the
106
- re-anchored backing before the swap, swap in one frame. (4) Windowed regions should be
107
- size-stable during interactive resize (GTK's "partial results drawn" is precisely an
108
- unexpected-jump failure) — or `winGravity: Unmap` them for the gesture.
109
- - **A's alloc-churn reduction is a stability fix as much as a CPU fix**: per-event garbage is
110
- what turns into GC pauses, and GC pauses are stutter. The acceptance criterion in §8.1 tracks
111
- bytes/event, not just µs/event.
112
- - **An adjacent lever this research surfaced but that needs no windows: the frame clock taxes
113
- every discrete input with up to one frame of latency.** Coalescing is right for motion;
114
- for discrete inputs (click, keypress, wheel) an immediate-flush policy — paint the damage from
115
- a discrete event as soon as the handler returns instead of waiting for the paced frame — is
116
- probably the single cheapest perceived-latency win in the whole stack, orthogonal to
117
- subwindows. Added to §8.1 as item 4b.
118
-
119
- ---
120
-
121
- ## 1. Where we are: the single-surface pipeline and what it costs
122
-
123
- One ntk `Window` per `<window>`/`<popup>`. All controls are retained JS nodes painted through one
124
- 2d context whose XRender Picture targets the window's **backing pixmap**
125
- (`ntk/lib/renderingcontext_2d.js:198-218`); ntk presents by `CopyArea(backing → window)` per dirty
126
- rect and serves real server Expose from the pixmap with zero JS when the backing is valid
127
- (`ntk/lib/window.js:652-658`).
128
-
129
- ### 1.1 Events
130
-
131
- `EventManager.attach()` subscribes mousedown/up/move/out + keys + focus **unconditionally**
132
- ([events.js:84](../../src/events.js)), so every window selects `PointerMotion` over its whole area
133
- whether or not anything wants hover. ntk coalesces motion keep-last per paced 16 ms frame, so the
134
- expensive work runs at frame rate, not device rate. Per motion frame:
135
- `hitTest` ([nodes/hittest.js](../../src/nodes/hittest.js)) does a front-to-back recursive descent calling
136
- `paintOrder()` — three array allocations, a per-child wrapper object, and a sort — _at every
137
- visited node_, and only prunes a
138
- subtree when the point is outside **and** the node clips ([nodes/hittest.js](../../src/nodes/hittest.js));
139
- non-clipping rows recurse into all children regardless. Then the hover path is diffed, `_path` is
140
- computed twice (hover + dispatch), and `:hover` flips invalidate.
141
-
142
- **Measured (in-process server, real renderer, 800×600 window, R×C grid of hover-styled boxes):**
143
-
144
- | controls | `_onMouseMove`, no hover change | alternating cells (hover flip) | garbage/event |
145
- | -------- | ------------------------------- | ------------------------------ | ------------- |
146
- | 100 | 3.3 µs | 4.4 µs | ~2–6 KB |
147
- | 1000 | 27.3 µs | 29.0 µs | ~8–11 KB |
148
- | 5000 | 132 µs | 135 µs | ~120 KB |
149
-
150
- ~94% of every motion event is the hit test; the hover diff machinery itself is ~1.7 µs. Scaling is
151
- linear in N (~27–55 ns/control/event). Worst case is the **top-left** control, not bottom-right:
152
- `hitTest` scans paint order back-to-front (front-to-back in z), so the first-painted control is
153
- tested last — 57 µs at 1000. Every event also allocates a closure-laden synthetic event object.
154
-
155
- ### 1.2 Damage and paint
156
-
157
- Damage is a capped list of ≤4 disjoint rects at the window root with least-waste merging
158
- ([nodes/damage.js](../../src/nodes/damage.js)). A hover-flip frame produces one coalesced ~65×16 rect and
159
- 31 ctx calls — the damage _narrowing_ works well. The cost that remains is the walk that exploits
160
- it: culling tests each candidate child via `_subtreeBounds()`, which is recursive and deliberately
161
- uncached, so repainting a 65×16 rect at 1000 controls made **2158 recursive `_subtreeBounds`
162
- calls**. Measured hover-flip frame: ~69 µs flush, split ≈ 55% cull walk / 30% request emission /
163
- 15% bookkeeping; a full-window repaint for comparison is ~1 ms. End-to-end hover flip
164
- (event + frame) ≈ **105–130 µs of main-thread JS at 1000 controls** — fine at 60 Hz, but all of it
165
- is O(N) walks that don't shrink when the damage does.
166
-
167
- Two ntk-side notes that surfaced: (a) each damage pass's first `ctx.clip()` rasterizes a
168
- **full-surface** a8 clip mask (`renderingcontext_2d.js:992-1005`) — an O(window-area) server cost
169
- even for a 20×20 rect; (b) `'draw'` with invalid backing is always full-window — expose-rect
170
- granularity exists only when the backing is valid.
171
-
172
- ### 1.3 Layout
173
-
174
- One `needsLayout` boolean per window; a layout frame runs yoga (`yoga-layout@3.2.1` **wasm**,
175
- shared with ntk) once at the root, then an O(N) `absolutize` walk — 4 embind crossings per node —
176
- regardless of what changed. **Every wheel notch is `invalidate(true, …, 'scroll')` — a full-tree
177
- relayout + absolutize per notch** ([nodes/layout.js](../../src/nodes/layout.js)), because scroll offset is
178
- baked into `abs` during absolutize. The scroll-blit fast path then tries to narrow the repaint,
179
- behind ~12 gates and a per-frame O(N) `_scrollBlitSafe` walk — and is currently **inert at
180
- runtime** (installed ntk 4.2.0 has no `scrollRegion`; the chain ntk#139 → release → #137/#139 is
181
- still in flight).
182
-
183
- ### 1.4 Wire (protocol bench, in-process server, counted with the repo's own `xcount.js`)
184
-
185
- | scenario | requests | bytes out | Composite px |
186
- | --------------------------------------------------- | ----------------------- | ------------------------ | -------------------------------------------- |
187
- | mount 1000-control grid | 1038 | 36.8 KB | 640 K |
188
- | hover flip to adjacent control (today) | ~14 net | ~400 B | 2.6 K |
189
- | 8 motion samples inside one cell (steady hover tax) | 0 | 0 | 0 — but 32 B _in_ + 1 JS hit test per sample |
190
- | create+map 1000 child X windows | 2002 | 52 KB | 0 |
191
- | move one child (ConfigureWindow) | 1 | 20 B | 0 |
192
- | move all 1000 (ntk as-is) | 2002 + **1001 replies** | 24 KB | 0 |
193
- | move all 1000 (`frameSync:false`) | 1002 | 20 KB | 0 |
194
- | windowed hover: crossing a child boundary | **0** | 0 (64 B in: Leave+Enter) | 0 |
195
-
196
- The `move all 1000` row exposes an ntk design cost: each ntk `Window` has its own frame clock and
197
- arms a `GetInputFocus` fence when its ConfigureNotify arrives — a 1000-child move storm doubles
198
- request count and adds 1000 replies. Any windowed-region design must share one frame clock per
199
- top-level (or create children `frameSync:false, coalesceEvents:false`).
200
-
201
- ### 1.5 What already exists
202
-
203
- Four kinds of real X windows already work: nested `<window>` (prop-driven geometry, stacking
204
- synced via `_restackWindowChildren` with server-verified QueryTree tests), `<popup>`
205
- (override-redirect, focus-delegating), `<glarea>` (the **only layout-driven child window** —
206
- `absolutize → _syncGeometry` diffs the rounded rect and issues one setState; clipping vs
207
- scroll panes unsolved, stacking vs `<window>` siblings deliberately undefined), and foreign windows
208
- (the WM example adopts other clients' windows). `NEXT_STEPS.md` §4 already states the policy this
209
- document mostly re-confirms: a real window **only when the server gives something the drawn path
210
- cannot** — "never because it has event handlers or a background color."
211
-
212
- The isWindow boundary is already carved through every subsystem (yoga membership, absolutize,
213
- `_subtreeBounds`, `_setRoot`, `paintOrder`, blit safety) — a "windowed region" is a drawn subtree
214
- given the WindowNode treatment. The genuinely missing piece is small: a yoga node that stays in
215
- the **parent's** layout tree whose computed rect is pushed to the child window as ConfigureWindow
216
- (glarea's `_syncGeometry` generalized), plus a paint root that isn't a full WindowNode
217
- (no WM hints, shared frame clock).
218
-
219
- ---
220
-
221
- ## 2. What the server actually offers (spec-verified)
222
-
223
- All claims below were verified verbatim against the X11R7.7 protocol spec (section names cited
224
- in the research notes; spec source URLs in §11).
225
-
226
- **Event routing.** The server hit-tests: pointer events originate at the deepest containing window
227
- and propagate **up** to the first window selecting the type; `do_not_propagate_mask` blocks
228
- per-type. There is no capture phase — React capture stays client-side. The **`child` field** of
229
- every delivered pointer event names the immediate child containing the pointer even when _no_
230
- child selects anything: with one flat layer of region windows this is a free server-side hit test
231
- one level deep, delivered on the events we already get. node-x11 parses it; ntk and react-x11
232
- currently discard it.
233
-
234
- **Enter/Leave.** Delivered only to windows selecting them, never propagated, never compressed.
235
- Sibling→sibling crossing = exactly 2 events (Nonlinear pair), nothing on the parent; nested
236
- crossings add Virtual events on strictly-intermediate windows. The `detail` field
237
- (Ancestor/Inferior/Virtual/Nonlinear…) natively encodes the DOM mouseout-vs-mouseleave
238
- distinction react-x11 synthesizes by path-diffing; `mode` distinguishes real crossings from
239
- Grab/Ungrab pseudo-crossings. Thin siblings skipped between pointer samples generate no events —
240
- crossings are computed between successive positions, not along the geometric path.
241
-
242
- **Implicit grab.** ButtonPress starts an automatic grab: all drag motion and the release deliver
243
- to the press's event window (negative coords when outside) until buttons release. This reproduces
244
- DOM implicit pointer capture _and_ "hover stays put during drag" server-side — react-x11 already
245
- silently relies on it at the whole-window level (`_pressOutside`, scrollbar drags).
246
-
247
- **InputOnly windows.** Depth 0, border 0, only win-gravity/event-mask/do-not-propagate/
248
- override-redirect/cursor attributes (anything else is BadMatch). Invisible to all rendering:
249
- never Expose, never occlude output, cannot be drawables. They own the pointer where they cover:
250
- local event masks, per-region cursors, server-side Enter/Leave — with **zero** paint-model
251
- consequences. Gotcha: they occlude _input_ from siblings beneath them; overlap must mirror hit
252
- priority in stacking order, and non-rectangular hit areas need SHAPE input regions instead.
253
-
254
- **Gravity.** `winGravity` repositions children on parent resize by the 8 compass deltas (+Static,
255
- +Unmap which auto-unmaps for "client will relayout anyway"); `bitGravity` retains window bits.
256
- `winGravity` was live-probed and works on both XQuartz and Xvfb — quartz-wm's own growbox is a
257
- SouthEastGravity child and its title-bar prelight is an InputOnly tracking window, so it is
258
- exercised daily on our platform. `bitGravity` is implemented in Xorg's mi layer
259
- (source-verified, `miSlideAndSizeWindow`) but was **not** probed on XQuartz's rootless resize
260
- path. Gravity moves, never resizes; servers may legally ignore bitGravity.
261
-
262
- **Scroll-by-move.** "If … a window is moved … the contents of the window are not lost but move
263
- with the window" — the server blits, and Expose covers only newly-valid regions. This is the
264
- protocol-guaranteed version of scroll-blit. Coordinate limits: positions are INT16 (±32767);
265
- sizes are CARD16 (≤65535 on the wire) but effectively bounded by the 16-bit coordinate/region
266
- space — treat ±32767 as the safe envelope for any window geometry.
267
-
268
- **Backing store.** Advisory in the spec; in practice a **majority-target capability**. Modern
269
- Xorg (≥1.15) implements it via per-window Composite automatic redirection — a full window-sized
270
- server pixmap each — and any Composite-capable server advertises `WhenMapped` (verified live: a
271
- `WhenMapped` child on Xvfb survived occlude-then-reveal with zero Expose). Since most expected
272
- users run Xorg+compositor or XWayland, **server-side content preservation for opted-in region
273
- windows is available on the deployment majority**, feature-detectable for free from the
274
- connection setup's per-screen `backing-stores` field. The exception is XQuartz — this dev
275
- machine — which ships without Composite: no core backing store at all (probe confirmed: the same
276
- `WhenMapped` child Exposed on reveal), and its rootless frame buffer covers _toplevel_ occlusion
277
- only. `save-under` is hardwired dead everywhere. Design consequence: treat server backing store
278
- as a **feature-detected acceleration tier** — on Composite servers it can carry a region's
279
- content preservation outright (and should _replace_, not double, ntk's backing pixmap for that
280
- window, since both cost the same window-sized pixmap); ntk's own backing pixmap remains the
281
- portable fallback that keeps XQuartz correct. Don't sprinkle it on small controls (a hidden
282
- pixmap + Damage record per window); use it at region granularity.
283
-
284
- **Translucency — the hard rendering limit.** Core X is a pixel-ownership model: a child window
285
- _replaces_ parent pixels where mapped; there is no core operation blending child over parent. An
286
- ARGB child overwrites rather than blends even under a compositor (compositors redirect and blend
287
- **top-level hierarchies only**; children are flattened inside by core clipping). The only route is
288
- Composite manual redirection + client-side Render — i.e. rebuilding by hand the compositing the
289
- single drawn surface gives for free (available on the Composite majority, impossible on XQuartz —
290
- so even as an advanced option it cannot be relied on universally). SHAPE gives non-rectangular
291
- boundaries but binary/aliased ones. **Opaque + rectangular is the promotion boundary**; shadows, rounded corners, translucent
292
- overlays, cross-fades disqualify a subtree.
293
-
294
- **Costs.** CreateWindow 32+4n B, ConfigureWindow 12+4n B (move = 20 B), Map/Unmap/Destroy 8 B,
295
- all round-trip-free; DestroyWindow is recursive (one 8-byte request tears down a subtree,
296
- measured 20 000 windows in 3.9 ms). XIDs: 29-bit space, 2 097 152 per client at Xorg defaults
297
- (protocol floor: 2^18). **node-x11's `AllocID` has an explicit unhandled-overflow TODO and ntk
298
- never releases window IDs** (it does for pixmaps/pictures/glyphsets and cursor GCs; the 2d
299
- context's GC and the present/clear GCs also never release) — harmless today, a real leak under
300
- window churn; the XC-MISC binding already exists in node-x11, just unwired.
301
-
302
- ---
303
-
304
- ## 3. Server reality on our targets (measured)
305
-
306
- **Target weighting.** Most expected users run Linux Xorg (with a compositor) or XWayland —
307
- Composite present, backing store available, standard DDX paths. XQuartz is where development
308
- happens but is a **minority deployment**: a custom rootless server without Composite, ~2× the
309
- dix cost floor on window storms, and its own bug surface. Read the numbers below accordingly:
310
- the Xvfb column approximates what the majority sees (a stock-Xorg dix floor); the XQuartz column
311
- is the compatibility floor the design must not break, **not** the environment to optimize for.
312
-
313
- ### 3.1 Per-window server cost — a non-issue
314
-
315
- | measure | value | source |
316
- | ------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
317
- | heap per plain window (Xorg 21.1.23, exact malloc accounting) | **293 B** (256 B WindowRec+privates + 32 B resource + hash growth) | heap-tool diff, 20 000 windows |
318
- | with WindowOptRec (cursor/backing/shape/…) | +96 B exact | same |
319
- | RSS per unmapped window, live XQuartz | ~250–460 B | two independent runs disagree 1.8× (10k- and 20k-window deltas); macOS RSS is compression-noisy — heap figure above is authoritative |
320
- | RSS per mapped window, XQuartz | ~1.3 KB | 5000-window delta (clip data included) |
321
- | clip fragmentation from 10 000 randomly overlapping siblings | ~3.7 B/window | 1-box/empty regions stored inline; fully-obscured = empty clip |
322
-
323
- Even 20 000 windowed regions ≈ 5–25 MB server-side. Memory never binds; CPU does:
324
-
325
- ### 3.2 The algorithmic costs — where window-per-control dies
326
-
327
- | operation | XQuartz | Xvfb (Xorg dix floor) |
328
- | ---------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ |
329
- | CreateWindow, batched | ~2–7 µs/win | ~1–4 µs/win |
330
- | MapWindow storm, 1000 / 5000 mapped children | 12–37 µs/win / **91 µs/win (455 ms)** | 10 µs / 42 µs/win |
331
- | ConfigureWindow move, mapped, 1000 / 5000 siblings | 21–53 µs/op / **131 µs/op (654 ms total)** | 15–18 µs / 68 µs/op |
332
- | same, unmapped (control) | ~4 µs/op | — |
333
- | raise 1 of 5000 siblings | 1.3 ms | 0.09 ms |
334
- | map / move **one** window among N mapped siblings | — | 0.13–0.22 ms @1k → 1.0–1.4 ms @20k (O(N)) |
335
- | server event pick (miSpriteTrace), flat worst case | — | **1.5 µs @100 → 60 µs @20k motions (~3 ns per mapped sibling, ≈6 ns per sibling scanned)** |
336
- | scroll = move **one** container child (1000 grandchild windows riding) | 4.9 ms/frame | 0.27 ms/frame |
337
- | parent toplevel resize | **14–20 ms regardless of children** | 0.14–0.46 ms |
338
- | destroy 10–20k subtree (one request) | 4–8 ms | 3.9 ms |
339
-
340
- Readings: clip revalidation (`miValidateTree`) is O(mapped siblings) per configure/map and the
341
- storms are superlinear; XQuartz is ~2× the dix floor and its rootless toplevel-resize path is its
342
- fragile spot (multiple crash bugs filed). Server-side _picking_ is effectively free and
343
- hierarchy-aware. InputOnly children: 1000 created in ~2 ms on XQuartz, zero errors — and XQuartz's
344
- own WM depends on them.
345
-
346
- **The 4.9 ms scroll row needs care:** that pane carried 1000 windowed grandchildren — the
347
- anti-pattern — plus a full XSync round trip per frame; how much of the 4.9 ms is per-child clip
348
- work vs pane-area blit was **not isolated** (a §10 follow-up; miComputeClips also has a VTMove
349
- translate-only fast path, so "revalidated 1000 clips" is not established). A content pane with
350
- _drawn_ rows should be one window move — expected µs-scale by extrapolation from the small-window
351
- move rows, but **not yet measured for a large pane**; measure pane-area scaling before committing
352
- (§10). The design rule stands: **windowed viewport + windowed content pane + drawn rows.**
353
-
354
- ### 3.3 Compositor interaction
355
-
356
- On Xorg+compositor and XWayland, child windows never reach the compositor — redirection captures
357
- whole toplevel hierarchies (children are flattened into the toplevel's pixmap server-side). On
358
- XQuartz likewise only toplevels get Quartz frames. So windowed regions cost the native window
359
- system nothing anywhere; their price is X-server clip CPU. Also verified: XWayland creates
360
- `wl_surface`s only for InputOutput children of root; and GLX **into child windows** is XQuartz's
361
- historically buggiest path (Wine crashes, Mesa fix in 2.7.4) — relevant to `<glarea>`, which sits
362
- exactly there. One majority-target upside worth naming: XWayland and composited Xorg are
363
- standard xserver builds, so the Composite-based backing store verified on Xvfb is expected to
364
- work there too (dix-level machinery; XWayland itself unprobed — §10).
365
-
366
- ---
367
-
368
- ## 4. What 30 years of toolkits concluded
369
-
370
- - **Xt/Motif**: window-per-widget → per-widget ConfigureWindow storms on every relayout (layout
371
- was client-side anyway), clear-then-expose flicker. Motif's windowless _gadgets_ are the
372
- cautionary inverse: the manager selected **all** events including MotionNotify and hit-tested
373
- client-side — exactly react-x11's current shape — and by X11R5 "gadgets are worse than widgets
374
- … from a performance perspective." The balance is empirical and era-dependent; Motif got it
375
- wrong in both directions.
376
- - **GTK 2.18 client-side windows (2009)**: kept the entire GdkWindow API — per-region event
377
- masks, enter/leave, grabs, scroll-copy — as a _client-side abstraction_; the real window carries
378
- the union of descendant interests. What broke was almost entirely native-handle consumers
379
- (plus expose-handler timing assumptions and a long-broken win32 backend).
380
- Larsson's flicker post is the canonical description of the scroll-copy tearing hazard the
381
- scroll-blit branch handles. Original driver: offscreen rendering/transforms, which per-widget X
382
- windows pin to the screen.
383
- - **GTK4 (2020)**: no subwindows at all, even client-side. One GdkSurface per toplevel/popup;
384
- cached per-widget render nodes; frame clock; pure client-side picking with synthesized
385
- crossings. GTK4's considered answer to hover-on-1-of-1000 is "hit-test every motion
386
- client-side, make picking cheap."
387
- - **Qt 4.4 alien widgets (2008)**: native-off by default; multi-granularity opt-in
388
- (env var / app / `WA_NativeWindow` / **implicit promotion on `winId()`** — asking for the handle
389
- _is_ the opt-in). Native promotion is viral to ancestors (clipping/stacking need it) unless
390
- explicitly stopped — a real API-design cost to remember. Where native reenters
391
- (`createWindowContainer`), the docs read like our glarea caveats: "stacks on top … as an opaque
392
- box," undefined stacking among containers.
393
- - **AWT/Swing**: the mixing/"airspace" problem — heavyweights always above lightweights,
394
- punching through menus and tooltips; took Sun 15 years to paper over via window shaping.
395
- Structurally identical to `<glarea>` today.
396
- - **Browsers/Flutter**: layers, not windows. Promotion criterion that survived: **different
397
- update cadence, moves-without-repaint, or foreign content** — never "is interactive." Both
398
- explicit hints (`will-change`, `RepaintBoundary`) and automatic triggers exist, with squashing
399
- as the defense against layer explosion; Flutter auto-inserts boundaries around scrolling list
400
- children. Flutter is the existence proof that event routing needs no windows at any scale.
401
-
402
- **No toolkit that left window-per-widget ever went back.** And none ever adopted InputOnly
403
- hit-region children for controls — that path is unexplored, not discredited.
404
-
405
- ---
406
-
407
- ## 5. The five concern axes
408
-
409
- ### 5.1 Local event masks
410
-
411
- **Concern:** one hover-sensitive control in a 1000-control window forces PointerMotion on the
412
- whole window and a hit test per motion.
413
-
414
- **True today,** but decompose the cost:
415
-
416
- - _Wire:_ 32 B/motion sample in, zero out (H1b). At 125–1000 Hz device rate, coalesced to frame
417
- rate by ntk. Negligible locally; matters over SSH-forwarded X.
418
- - _CPU:_ ~27 µs + 8–11 KB garbage per motion frame at 1000 controls — all in `hitTest`/`paintOrder`
419
- churn. This is a **client-side data-structure problem**: `paintOrder()` re-allocates and
420
- re-sorts per node per event; non-clipping containers don't prune; `_path` runs twice.
421
- A cached paint-order array (invalidated on child-list/zIndex change), point-in-subtree-bounds
422
- pruning (using bounds cached during `absolutize`), and a reused event object would cut this
423
- 10–50× with zero protocol change. GTK3's union-mask + client routing and GTK4/Flutter picking
424
- are precedents that this is enough at any realistic scale.
425
- - _What subwindows add on top:_ zero motion traffic at the server (mask off PointerMotion
426
- entirely unless someone has `onMouseMove`), Enter/Leave computed by the server at ~6 ns/sibling,
427
- the free one-level `child` field even without any child selecting events, per-region cursors as
428
- a creation attribute, correct hover when content moves _under_ a stationary pointer (scroll —
429
- the client model gets this stale today), and implicit-grab press/drag/release per control.
430
-
431
- **Also true:** ntk's per-region event-mask machinery (lazy mask growth from listeners) already
432
- exists and react-x11 defeats it by subscribing everything unconditionally
433
- ([events.js:84](../../src/events.js)). Handler-counted lazy subscription (the r3f rule `<glarea>`
434
- already implements one level up) would drop `PointerMotion` from windows with no motion/hover
435
- consumers at all — the cheapest win of the lot.
436
-
437
- **Latent bug either way:** `_onMouseOut` clears the entire hover path on _any_ LeaveNotify
438
- without checking `detail`/`mode` ([events.js:384](../../src/events.js)). Today every trigger —
439
- pointer leaving the toplevel (the common case), crossings into `<glarea>`/nested `<window>`
440
- children, Grab/Ungrab pseudo-crossings from popup grabs — happens to clear hover acceptably;
441
- the first windowed region would break hover globally. Any subwindow work starts by making
442
- crossing handling detail/mode-aware.
443
-
444
- **Verdict:** fix client-side first; subwindow event masks are not the remedy for the CPU concern.
445
- InputOnly regions (§8, option D) are the right vehicle _if_ the wire/cursor/scroll-hover benefits
446
- are wanted, because they carry none of the rendering costs.
447
-
448
- ### 5.2 Damage area calculation
449
-
450
- Damage narrowing already works (one coalesced rect, 31 ctx calls for a hover flip). What
451
- subwindows would scope is the _walk_: per-region damage lists mean a region's invalidate touches
452
- only its subtree, and the 4-rect cap stops being shared across unrelated corners of the UI. But
453
- the same effect is available client-side by caching `_subtreeBounds` during `absolutize` (the
454
- "recomputed on demand" design assumption — bounds asked only by the handful of invalidating nodes
455
- — is falsified by measurement: 2158 recursive calls per 65×16 repaint, 55% of frame time).
456
-
457
- What subwindows do **not** fix: the parent paints into a _pixmap_, and pixmaps have no children —
458
- X's child-clipping applies only at the final CopyArea, so parent rasterization under windowed
459
- children still happens unless the client culls it (today's `_outsideDamage`, retargeted at region
460
- rects). A per-region window also brings per-region present (CopyArea) and — critical on the ntk
461
- side — a per-Window frame clock and fence with **no cross-window frame transaction**: two regions
462
- presenting independently can tear against each other. One ntk frame clock per toplevel is a
463
- prerequisite (§9).
464
-
465
- **Verdict:** cache subtree bounds client-side; per-region damage is a real but secondary benefit
466
- that falls out of any region window added for other reasons (scroll), not a reason by itself.
467
-
468
- ### 5.3 Backing store invalidation
469
-
470
- ntk already double-buffers per window and serves Expose from backing with no JS. Windowed regions
471
- would each carry a backing pixmap — that is the memory axis: viewport-sized today vs
472
- region-extent-sized after promotion. A 900×90 000 content pane is absurd (324 MB) regardless of
473
- who holds the pixmap; the design must band the backing, opt the pane out (`backingStore:false` +
474
- JS repaint of exposed strips), or size the pane viewport+overdraw and re-anchor.
475
-
476
- **Who holds the pixmap is a per-server choice, and the majority target has the better option.**
477
- On Composite servers (most users: Xorg+compositor, XWayland), setting `backing-store: WhenMapped`
478
- on the content pane makes the _server_ retain the pane's clipped-away band — so scroll-by-move
479
- generates **no Expose at all**: the server blits the surviving band and restores the revealed
480
- strip from its own backing (this is exactly the behavior the in-process JS server showed as the
481
- subwindow best case, and it is real on the majority target — occlude/reveal probe passed on
482
- Xvfb). ntk's backing pixmap for that window then becomes redundant and should be dropped
483
- (`backingStore:false`), or memory doubles for no benefit. On XQuartz (minority; no Composite)
484
- the same window falls back to ntk's pixmap with Expose-strip repaints served from it. Both modes
485
- cost one window-sized pixmap; the difference is which side owns it and whether scroll exposes
486
- reach the client at all. Feature-detect via the screen's `backing-stores` field. Either way,
487
- don't put backing store on small controls — on Xorg each opted-in window is a hidden full-window
488
- redirect pixmap plus a Damage record.
489
-
490
- There is also a fix available with no windows at all — but it starts in ntk, not react-x11: ntk
491
- 4.2.0 collapses every invalid-backing restore to a full-window synthetic `'draw'` (no rects reach
492
- the renderer at all). Delivering the coalesced expose-rect union (§9.7) plus a react-x11 consumer
493
- would restore partial granularity without any windows.
494
-
495
- ### 5.4 Layout engine cache
496
-
497
- The key structural fact (verified in code): **a region's inner layout depends only on its
498
- width/height, never its position** — position enters solely as the absolutize origin, and a
499
- WindowNode lays out from `{0,0,w,h}`. So for a size-stable region, a parent-driven move is
500
- exactly one ConfigureWindow with zero inner relayout and zero repaint (the backing rides along).
501
- `_sizePinned` + the `_reflowed` contained-reflow machinery are the embryo of this boundary; a
502
- windowed region is `_sizePinned` made real, enforced by the server instead of damage arithmetic.
503
-
504
- The scroll pane is where this bites hardest today: scroll offset baked into `abs` makes every
505
- wheel notch a full-tree relayout + absolutize (~4 embind crossings × N per notch). A windowed
506
- scroll pane deletes that: scroll stops touching layout at all. Short of windows, the same cheap
507
- observation — "scroll is a translation, not a reflow" — could be exploited client-side by storing
508
- scroll offset as a paint/hit-test-time translation instead of re-absolutizing, at the cost of
509
- threading an offset through paint and hitTest (a mini transform stack — the thing the flat `abs`
510
- design deliberately avoids). That refactor is the client-side alternative to compare against.
511
-
512
- When a parent resize _does_ change a region's size: two frames of latency (parent pass →
513
- ConfigureWindow → child relayout) unless the inner pass runs synchronously in the same flush —
514
- resize asynchrony between parent and children is literally GTK's stated top reason for leaving
515
- subwindows ("asynchronously resize all child windows and sometimes get partial results drawn").
516
- Regions should be size-stable or few.
517
-
518
- ### 5.5 Server-supported layout (gravity etc.)
519
-
520
- Works (both servers, quartz-wm depends on it), already forwardable through ntk creation
521
- attributes from JSX props, and nearly useless _as a layout engine_ for yoga: it expresses
522
- corner/edge pinning of fixed-size children only — no stretch, no flex, no percentages, no
523
- space-between; and yoga's default cross-axis is `stretch`. Its honest use is latency-masking
524
- anchored panes during interactive resize (child snaps with the edge server-side, one frame
525
- before the client's authoritative ConfigureWindow), plus `winGravity: Unmap` to suppress
526
- stale-position jumps when relayout is coming anyway. **Under the project's goal hierarchy that
527
- is not a dismissal — "latency mask" is the main goal during a resize gesture** (§0.1): resize is
528
- continuous direct manipulation, the client is structurally one frame late, and the server moving
529
- anchored windows instantly is "update instantly, catch up" in its purest form. It applies only
530
- to windows that already exist for other reasons (a windowed scroll pane's panes and bars, capability
531
- windows) and only to gravity-expressible positions — so it is a free rider on B, never a reason
532
- to create windows. ntk gap: gravity is creation-time only (no ChangeWindowAttributes path
533
- exposed; §9.10).
534
-
535
- ---
536
-
537
- ## 6. When a windowed region pays — ranked
538
-
539
- | candidate | why | verdict |
540
- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
541
- | **Scrollview (viewport + content pane)** | server-side blit + strip expose deletes the whole scroll-blit gate stack, the O(N) safety walk, per-notch relayout; drawn rows ride along free; zero-Expose scroll with server backing store on Composite targets (the majority) | **the** case — prototype behind a flag (§8 B) |
542
- | **`<glarea>` / video / foreign embed** | server capability the drawn path cannot express | already exists; keep implicit-by-capability |
543
- | **`<popup>` / nested `<window>`** | WM interaction, bounds escape | already exists |
544
- | InputOnly hit regions (cursor, hover-during-scroll, remote-X wire, drag semantics) | zero rendering cost; all event-locality wins | optional spike (§8 D) — no toolkit precedent either way |
545
- | Large opaque panes with independent update cadence (dashboard tiles, terminal grid, animation layer) | per-region damage + present isolation | only after frame-clock sharing exists in ntk; measure first — client-side fixes may suffice |
546
- | Hover-dense control grids | the motivating concern | **no** — fix the client-side hit test (§5.1) |
547
- | Table/list rows | GTK's treeview-window-per-row is the canonical regret | **no** |
548
- | Anything translucent, shadowed, rounded-antialiased, transform-animated | core X cannot blend child over parent | **structurally excluded** |
549
-
550
- ---
551
-
552
- ## 7. What breaks — the hazards checklist
553
-
554
- Every item below is verified (code, spec, or measured), not speculative:
555
-
556
- 1. **Airspace**: a windowed region is always above all drawn content of its parent; drawn
557
- tooltips/menus/overlays cannot paint over it. Mitigation: overlays are already `<popup>`s in
558
- react-x11 — the airspace rule aligns with existing practice, but must be documented per region.
559
- 2. **Translucency/rounding**: opaque rectangles only (§2). Antialiased rounded corners against
560
- the parent are impossible (SHAPE is binary).
561
- 3. **Hover detail/mode blindness**: `_onMouseOut` clears all hover on any LeaveNotify —
562
- pre-existing bug, first windowed region trips it ([events.js:384](../../src/events.js)).
563
- 4. **ntk InputOnly BadMatch**: ntk unconditionally puts `bitGravity: 1` in every CreateWindow
564
- value list (`ntk/lib/window.js:375-381`) — illegal for InputOnly; needs an ntk guard (plus a
565
- `getContext` guard).
566
- 5. **Per-window frame clocks** (a _stability_ requirement under §0.1, not just perf — two
567
- regions presenting independently in one gesture is visible tearing): N regions = N
568
- fences/timers, ordering inversions possible
569
- (move(A) → press(B) can invert since coalescing is per-window), storms double request counts.
570
- Needs a shared per-toplevel frame clock in ntk.
571
- 6. **XID leak**: ntk never releases window IDs; node-x11 AllocID overflow is an unhandled TODO.
572
- One-line ntk fix + optionally wire the existing xc-misc binding.
573
- 7. **16-bit geometry**: positions INT16 (±32767), sizes CARD16 — treat ±32767 as the envelope;
574
- content panes for long lists must re-anchor;
575
- glarea already has this bug latent (no clamp/unmap when scrolled far out — no guard exists).
576
- 8. **First-paint flash**: `attributes.backgroundColor` is dead in ntk 4.2.0 (no consumer);
577
- region windows should set `backgroundPixel` (or background None to inherit stale pixels
578
- rather than flash — the GTK2 anti-flicker rule).
579
- 9. **Resize asynchrony**: parent-size-tracking regions repaint one frame late (GTK's #1 reason
580
- for leaving). Keep regions size-stable; consider `winGravity: Unmap` for the rest.
581
- 10. **XQuartz specifics** (minority target, but the dev machine — compatibility floor, not
582
- design driver): no Composite → no server backing store for regions; child-GLX is the
583
- historically buggy path; toplevel resize is expensive and crash-prone; XTEST off by default
584
- (blocks input-injection tests on macOS — integration tests should target Xvfb/the JS server).
585
- 11. **Stacking bookkeeping**: regions must join `_restackWindowChildren`-style ordering
586
- (including vs `<glarea>`, whose stacking is currently undefined), and scroll pane clipping of
587
- a windowed region needs the server parent-child relationship to be the clip (viewport window),
588
- not `overflow: hidden` arithmetic.
589
- 12. **Coordinate-space audit**: `_pressOutside`, popup anchoring (`_screenOrigin` composition),
590
- DevTools `getClientRects`, ClickToComponent — everything reading `abs` as window-absolute
591
- needs the extra origin composed when a region boundary intervenes.
592
- 13. **Cross-boundary bubbling and focus**: today capture/bubble stops dead at every window
593
- boundary (`_path` breaks at the owning window, [events.js:130](../../src/events.js)) and
594
- `_focusables` refuses to walk into `isWindow` children ([events.js:567](../../src/events.js)) —
595
- a windowed scroll pane needs row clicks to bubble to ancestors outside the region and Tab to
596
- traverse into it. The existing precedents are popup-style `focusManager` delegation and the
597
- key path's tree-walking dispatch; both must be generalized to pointer events (with
598
- child-local → owner coordinate translation) before any region ships.
599
-
600
- ---
601
-
602
- ## 8. API design: the manual → automatic spectrum
603
-
604
- Ordered from least to most automatic, with verdicts:
605
-
606
- **A. Client-side fixes only (no new API).** Cache `paintOrder`; cache subtree bounds in
607
- `absolutize`; prune hit test by cached bounds; single `_path` per event; reused event objects;
608
- handler-counted lazy event masks (stop selecting PointerMotion when nothing consumes it); use the
609
- free `child` field once regions exist; expose-rect granularity from ntk instead of full-window.
610
- **Do this regardless.** It attacks every _measured_ cost, needs no ntk release, and no design
611
- risk. Expected effect: motion frame from ~27 µs to low-µs; hover-flip flush from ~69 µs to ~30 µs.
612
-
613
- **B. Framework-internal promotion at known hotspots (invisible API).** The scroll pane becomes
614
- `viewport window + content pane window + drawn rows` internally, feature-gated
615
- (`REACT_X11_WINDOWED_SCROLL=1` during bring-up, later a scroll pane prop, eventually default where
616
- safe). No user-facing "window" concept; identical JSX. This is Flutter's
617
- `addRepaintBoundaries`-around-scrollables move, and it lands on the only region with a measured
618
- structural win. Framed by the goals (§0.1): scroll is the most latency-sensitive gesture in the
619
- toolkit, and this makes its screen response synchronous with the input event (ConfigureWindow
620
- written in the wheel handler, before React) instead of one relayout + one paced frame later —
621
- with idle time spent prepainting the overdraw band so revealed content is already backed.
622
- On the deployment majority (Composite servers) it has a second tier for free:
623
- give the content pane `backing-store: WhenMapped` (feature-detected from the screen's
624
- `backing-stores` field, ntk backing dropped for that window) and scrolling stops generating
625
- Expose entirely — the server owns both the blit and the strip restore; XQuartz falls back to
626
- strip repaints from ntk's pixmap. Bring-up order: needs the ntk frame-clock/backing work (§9),
627
- the detail/mode hover fix, cross-boundary bubbling/focus (§7.13), INT16 re-anchoring, and honest
628
- fallbacks (small viewports and translucent-over-viewport layouts keep the drawn path — the
629
- scroll-blit gates shrink to "is this scroll pane windowed or not").
630
-
631
- Two things to say out loud about B. First, **it reverses a recorded decision**: NEXT_STEPS §4
632
- states verbatim that a scroll pane is "a clip rect + translation on the render list, not a child
633
- window (optionally optimized later with CopyArea scrolling inside the same window)" — the
634
- CopyArea option being exactly the scroll-blit branch B would supersede. This document argues the
635
- reversal on new measured evidence (server-side blit verified on both targets, per-notch relayout
636
- cost, the gate stack); if B lands, amend NEXT_STEPS §4. Second, **the hermetic test suite cannot
637
- exercise B's core mechanism**: the in-process JS server gives every window a retained raster, so
638
- child moves generate no Expose there — the expose-strip half of the design is invisible to it,
639
- XTEST is absent on this XQuartz, and there is currently zero test coverage for events/focus
640
- inside nested windows. B's test plan is Xvfb-based integration tests (the only full-fidelity
641
- target on this machine) plus extending the JS server with real expose-on-move semantics.
642
-
643
- **C. Explicit region element for capability, not performance.** Keep real windows where the
644
- server gives capability: `<glarea>` (exists), `<foreign windowId>` (NEXT_STEPS §4), possibly
645
- `<surface>` for an opaque pane an app _knows_ updates independently — with documented constraints
646
- (opaque, rectangular, above drawn siblings, clipped only by its parent window). Follow Qt's
647
- implicit-promotion insight: asking for the native handle (a ref that needs an XID) _is_ the
648
- opt-in signal. **Do not ship `<box windowed>` as a general perf knob** — the constraint list is
649
- exactly the airspace/translucency/stacking catalogue every toolkit documents as its regret.
650
-
651
- **D. InputOnly hit-region spike (orthogonal, optional).** A `hitWindow` internal experiment:
652
- InputOnly children mirroring a few interactive regions, selecting Enter/Leave/Button, drawn tree
653
- untouched. Buys per-region cursors as server attributes, hover-during-scroll correctness,
654
- implicit-grab drag semantics per control, zero motion wire, `do_not_propagate` shielding — at the
655
- price of ConfigureWindow-per-layout for those regions, XID lifecycle, and the detail/mode fix.
656
- No rendering hazards at all. The perceived-performance lens (§0.1) adds one argument the CPU
657
- lens missed: **a per-window cursor is applied by the server the instant the pointer crosses,
658
- with the client not in the loop at all** — cursor feedback (I-beam over text, resize arrows on a
659
- split handle) stays instant even while the client is mid-render or mid-GC, where today it rides
660
- the motion→hover→`setCursor` path and stalls with the client. Worth a spike _after_ A if cursor
661
- fidelity, hover-during-scroll, or remote-X matter; the CPU argument alone still does not
662
- justify it.
663
-
664
- **E. Fully automatic promotion (browser-style heuristics).** Rejected. Requires squashing
665
- heuristics, memory-pressure valves, and years of tuning; the failure modes (layer explosion) are
666
- documented at length by Chromium; and our candidate list (§6) is short enough that named hotspots
667
- cover it. History: only browsers ever made this work, and nothing at react-x11's scale should try.
668
-
669
- **Recommended sequence: A → B(prototype + measure) → C(as capabilities demand) → D(spike if
670
- motivated) — E never.**
671
-
672
- ### 8.1 Concrete action plan (issue-ready)
673
-
674
- Every item below is adoptable as code, API, or documentation without further research. Ordered by
675
- dependency; acceptance criteria reference the benchmarks preserved out-of-tree (§10). Items are
676
- phrased so they can be lifted directly into issue drafts (react-x11 / ntk / node-x11 as marked).
677
-
678
- **Phase 0 — record the decisions (docs).**
679
-
680
- - **[react-x11 docs]** Add a "when does an element get a real X window" section to
681
- `docs/elements.md`: the promotion policy (opaque + rectangular + region granularity; real
682
- windows for capability, never for "has handlers or a background"), the airspace constraints
683
- (currently documented only in `docs/glx.md`), and the hazard list from §7. This makes the
684
- policy user-visible instead of implicit in NEXT_STEPS.
685
- - **[react-x11 docs]** Annotate NEXT_STEPS §4's scroll pane sentence with a pointer to this
686
- document: the "clip rect + translation" decision is now conditional on the Phase-3 prototype's
687
- results, and should be amended (not silently contradicted) if B lands.
688
-
689
- **Phase 1 — client-side fixes, no ntk dependency (react-x11 code).**
690
-
691
- 1. **fix(events): respect crossing `detail`/`mode`** in `_onMouseOut`/hover handling
692
- ([events.js:384](../../src/events.js)) — drop Grab/Ungrab pseudo-crossings, treat
693
- `detail=Inferior` as "still inside". Prerequisite for every windowed anything; arguably a live
694
- bug for `<glarea>` + popup-grab edge cases now. Test: hover retention across a glarea/nested
695
- `<window>` crossing.
696
- 2. **perf(hit-test): cache and prune** — cache `paintOrder()` per node (invalidate on child-list/
697
- zIndex change), cache subtree bounds during `absolutize` (drop the per-call `_subtreeBounds`
698
- recursion), prune `hitTest` descent by cached bounds, build `_path` once per motion, reuse the
699
- synthetic event object. Acceptance: `hitbench.mjs` motion frame ≤5 µs and ≤1 KB alloc at 1000
700
- controls (from 27 µs / 8–11 KB); hover-flip flush cull share <20% (from ~55%).
701
- 3. **perf(events): handler-counted lazy event masks** — stop subscribing `mousemove`/etc.
702
- unconditionally in `EventManager.attach()`; select PointerMotion only while some node has a
703
- motion/hover/cursor consumer (the r3f rule `<glarea>` already applies one level up). Note the
704
- cursor dependency: `_updateCursor` rides the hover path, so `cursor` props count as consumers.
705
- Acceptance: a protocol-bench scenario asserting zero MotionNotify delivery for an app with no
706
- hover/motion/cursor usage.
707
- 4. **chore(windows): pass `backgroundPixel` at creation** (forwardable through ntk today) so
708
- windows stop flashing undefined content before first paint.
709
- 4b. **perf(latency): immediate flush for discrete inputs** (filed as
710
- [#141](https://github.com/sidorares/react-x11/issues/141)) — clicks, keypresses, and wheel
711
- currently paint on the next paced frame (up to 16 ms + fence later); flush their damage as
712
- soon as the discrete handler returns, keeping coalescing for continuous motion only. No
713
- windows involved; probably the cheapest perceived-latency win in the stack (§0.1).
714
- Acceptance: a caret blink / button `:active` flip reaches the wire in the same event-loop
715
- turn as the input event (assert with the protocol tracer); motion-driven hover still
716
- coalesces to frame rate.
717
-
718
- **Phase 2 — ntk/node-x11 groundwork (file as issues now, accumulate into the next ntk cycle).** 5. **[ntk fix]** Release window XIDs on destroy (pixmaps/pictures/glyphsets already do); note
719
- the 2d-context/present/clear GC ids also leak. **[node-x11 fix]** wire the existing
720
- `xc-misc` GetXIDRange binding into `AllocID` (overflow TODO at `xcore.js:377-389`). 6. **[ntk feat]** Shared frame clock/fence per top-level for child windows (or a supported
721
- `frameSync:false, coalesceEvents:false` child mode) — kills the measured 1000-fence storm
722
- (protobench E vs E2) and the cross-window event-ordering inversion. 7. **[ntk fix]** InputOnly (`windowClass: 2`) support: omit `bitGravity`/background attributes
723
- (BadMatch today), guard `getContext`, skip the StructureNotify base mask for micro-windows. 8. **[ntk feat]** `'draw'` with coalesced expose rects (today invalid-backing restores collapse
724
- to full-window), plus a react-x11 consumer — partial restores without any windows. 9. **[ntk feat]** `serverBackingStore` creation option mapping to the X backing-store attribute
725
- (deliberately not forwarded today, `window.js:112-121`), with feature detection from the
726
- screen's `backing-stores` field — enables the zero-Expose scroll tier on Composite servers. 10. **[ntk feat, optional]** post-create `ChangeWindowAttributes` for winGravity/bitGravity
727
- (TODO at `window.js:975`) — only if gravity latency-masking on anchored panes is pursued.
728
-
729
- **Phase 3 — the windowed scroll pane prototype (react-x11, flagged, decision gate).** 11. **feat(scroll pane): `REACT_X11_WINDOWED_SCROLL=1`** — viewport window + content pane window + drawn rows; pane sized viewport+overdraw with INT16 re-anchoring; scrollbars drawn on the
730
- viewport window; `serverBackingStore` tier on Composite servers, ntk-pixmap + expose-strip
731
- fallback on XQuartz. Two goal-driven behaviors are part of the design, not polish:
732
- **(latency)** the ConfigureWindow is written synchronously in the wheel/drag handler, before
733
- any React or layout work — scroll response never waits on the frame clock or a render;
734
- **(idle prep)** the overdraw band is prepainted during idle, and re-anchoring runs in idle
735
- time with the swap prepared off-screen so no seam is ever visible on the input path.
736
- Depends on items 1, 5–9. Test plan: Xvfb integration tests (the hermetic JS server cannot
737
- see expose-on-move; XTEST is absent on this XQuartz); screenshot parity vs the drawn path.
738
- Acceptance: **(latency)** wheel-to-ConfigureWindow-on-the-wire in the same event-loop turn,
739
- measured with the protocol tracer, vs today's relayout + paced frame; pure-scroll frame
740
- beats the scroll-blit path on the `protocol.js` bench metrics and on Xvfb frame times; zero
741
- Expose per notch with the backing-store tier; **(stability)** no visible re-anchor seam, one
742
- present per frame across viewport+bars (no cross-region tearing), hover changes from
743
- pane-moves-under-pointer debounced to frame rate; no parity regressions. **Decision gate:**
744
- if it wins, promote to a scroll pane prop and amend NEXT_STEPS §4; if not, this document's
745
- §5/§3 records why not, and the scroll-blit chain stands.
746
-
747
- **Phase 4 — optional, after Phase 3 evidence.** 12. **spike: InputOnly hit regions** (per-region cursors as server attributes, hover-during-
748
- scroll correctness, remote-X wire silence, implicit-grab drags) — sweep script ready in
749
- `windowed-regions-bench/bench1.js`, needs Xvfb+XTest. Adopt only if the measured benefits
750
- show up in a real app scenario. 13. **API decisions to take deliberately, not by drift:** `<surface>` element (explicit opaque
751
- pane) and ref-XID implicit promotion (Qt's `winId()` insight) stay out of the public API
752
- until a concrete capability demands them; `<box windowed>` stays out, period.
753
-
754
- ---
755
-
756
- ## 9. Prerequisites in ntk / node-x11 (accumulate into the next ntk cycle)
757
-
758
- (The same items appear inside §8.1's phased action plan with their react-x11 counterparts; this
759
- section is the ntk-release shopping list.) Fixes independent of any decision here are marked ✱.
760
-
761
- 1. ✱ Release window XIDs on destroy (one line); optionally wire `xc-misc` GetXIDRange into
762
- `AllocID` (binding exists, unwired; overflow TODO at `x11/lib/xcore.js:377-389`).
763
- 2. Shared frame clock / fence per top-level for child windows (or a supported
764
- `frameSync:false, coalesceEvents:false` child mode + parent-side coalescing) — kills the
765
- 1000-fence storm and the cross-window ordering inversion.
766
- 3. InputOnly support: omit `bitGravity`/background attrs when `windowClass === 2`; guard
767
- `getContext`.
768
- 4. ✱ Consume or remove the dead `backgroundColor` creation attribute (ntk-side; no consumer in
769
- 4.2.0). `backgroundPixel` itself is _already_ forwarded by ntk — the missing half is
770
- react-x11 passing a real value at region-window creation so new windows don't flash
771
- undefined content.
772
- 5. `ChangeWindowAttributes` exposure for winGravity/bitGravity post-create (TODO already at
773
- `window.js:975`) — only if gravity latency-masking is wanted.
774
- 6. Banded/opt-out backing pixmap strategy for large content panes (or document
775
- `backingStore:false` + expose-strip repaint as the supported mode).
776
- 7. ✱ Deliver ntk's coalesced expose-rect union to the renderer ('draw' with rects), so partial
777
- restores stop being full-window repaints.
778
- 8. ✱ Ship `scrollRegion` (ntk#139) — the current chain's blit remains the right fallback for
779
- non-windowed scroll panes.
780
- 9. A way to set the X `backing-store` attribute — ntk deliberately does **not** forward it
781
- (its own `backingStore` creation option means client-side double buffering, and the comment
782
- at `window.js:112-121` excludes the X attribute to avoid a silent collision). The
783
- zero-Expose scroll tier on Composite servers (§5.3, §8 B) needs an explicit spelling, e.g.
784
- `serverBackingStore: 'whenMapped'`, plus feature detection from the screen's
785
- `backing-stores` field so it degrades cleanly on XQuartz.
786
- 10. ✱ **Done (ntk#148, 5.2.0).** `Window.frameInFlight()` — whether the last frame's fence is still
787
- unanswered. Asked for by issue #141 (§0.1, §8.1 item 4b): a discrete input's response is
788
- painted from its own handler rather than scheduled, and this is the gate that decides
789
- between painting now and leaving it to the frame clock, so a burst still costs one
790
- catch-up frame instead of one paint per event.
791
-
792
- `_onMouseOut` detail/mode filtering is a react-x11-side prerequisite for **everything** above
793
- (and is arguably a live bug for `<glarea>` + popup-grab edge cases today).
794
-
795
- ---
796
-
797
- ## 10. Measurement appendix
798
-
799
- Scripts and raw research notes are preserved out-of-tree in the maintainer's local
800
- `docs-extra/` research area (deliberately uncommitted): `docs-extra/windowed-regions-bench/`
801
- holds the scripts, `docs-extra/windowed-regions-notes/` the eleven per-agent research reports
802
- with full source URLs. The in-process-server scripts need no display; the copies reference
803
- absolute paths from the research session and may need path tweaks to re-run:
804
-
805
- - `hitbench/hitbench.mjs` — hit-test/motion/flush microbench (in-process server + real renderer)
806
- - `protobench/protobench.mjs` — protocol-traffic scenarios (repo's `xcount.js` counting)
807
- - `livebench/bench1.js bench2.js bench3.js` — live XQuartz window storms, RSS, InputOnly (node-x11)
808
- - `bench.c bench2.c` — XQuartz-vs-Xvfb C benchmarks (storms are batched with a single trailing
809
- XSync — same methodology as livebench, which is what makes the merged ranges legitimate;
810
- XSync-per-op only for raise/resize/pick/scroll frames; gravity/backing probes, scroll-by-move)
811
- - `winmem2.js mapmem.js mapscale.js holdrand.js motion.js` — Xorg heap accounting, map scaling,
812
- miSpriteTrace pick cost (XTest, Xvfb)
813
- - x11perf-1.6.1 (built locally) — window-op and Copy/Composite throughput on XQuartz
814
-
815
- Key environment facts worth re-checking when numbers matter: XQuartz 2.8.6 here is a custom
816
- build (X.Org 21.1.23, no XTEST, **no Composite**); two Xvfb servers were running during
817
- measurement; the in-process JS X server gives every window a retained raster (universal backing
818
- store) and implements neither gravity nor Composite — its subwindow numbers are lower bounds.
819
-
820
- Follow-up measurements worth running before committing to B:
821
-
822
- - windowed-scroll pane prototype vs scroll-blit vs full repaint, same content, frame times +
823
- protocol counts — on XQuartz _and_ on a Composite target (Xvfb or a Linux box/VM with
824
- Xorg+compositor), including the `backing-store: WhenMapped` zero-Expose tier, which is the
825
- majority-user path (verified only as an occlude/reveal probe on Xvfb so far, not as a scroll
826
- loop; XWayland entirely unprobed). Re-save the bench baseline at the ntk floor bump, per the
827
- PR-chain convention;
828
- - XQuartz pane-move cost vs pane pixel area (isolate from the grandchild-count effect — the
829
- 4.9 ms row conflates them, and the "drawn-rows pane is µs-scale" expectation rests on it);
830
- - the un-measured layout axis: time a wheel-notch frame (full-tree relayout + absolutize) at
831
- N≈1000 with the hitbench harness, and settle whether yoga's wasm `calculateLayout` early-outs
832
- when the root size is re-set to identical values — it conditions the client-side
833
- "scroll-as-translation" alternative's payoff (§5.4);
834
- - Enter/Leave storm cost sweeping a grid of InputOnly regions (needs Xvfb + XTest, script ready
835
- in `livebench/bench1.js`);
836
- - hit-test bench re-run after the §8-A caching fixes to confirm the 10–50× expectation.
837
-
838
- Not covered here, noted for completeness: **Present** (advertised by XQuartz, rootless fidelity
839
- unknown) is the protocol's frame-transaction tool and the principled answer to §5.2's
840
- cross-region tearing if regions ever multiply; **XInput2** changes the event-mask story (raw
841
- motion, touch, smooth scroll) and its crossing semantics were not covered by this pass; the JS
842
- server implements neither, so both need Xvfb work.
843
-
844
- ## 11. Primary sources
845
-
846
- X11R7.7 protocol spec (event delivery, crossings, InputOnly, gravity, backing-store, encoding);
847
- Composite v0.4, Shape v1.1, Render v0.11, XC-MISC specs; xorg-server master sources
848
- (`windowstr.h`, `mivaltree.c`, `miwindow.c`, `compinit.c`, `dix/window.c`, `dix/dispatch.c`);
849
- XQuartz rootless README + quartz-wm sources (InputOnly tracking window, SouthEastGravity growbox);
850
- xwayland-window.c; Larsson's CSW posts + gtk-devel rationale mail; GTK3/GTK4 input & drawing
851
- docs; Qt QWidget "Native vs Alien" docs + window-embedding blog; O'Reilly Motif ch.3 (gadgets)
852
- and Java Swing ch.28 (mixing); Chromium GPU-compositing design doc; Keith Packard's smart
853
- scheduler paper; XQuartz issues #308/#438/#466 and old-ticket 536 (child GLX). Full URL list in
854
- the out-of-tree research notes (§10).