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/windows.md CHANGED
@@ -1,1404 +1,380 @@
1
1
  # A native Windows backend
2
2
 
3
- **Status: phases 0–2 are built, on hardware.** Written 2026-09-11 as a
4
- research PRD against react-x11 2.11.0 (`13ffb34`) — on a Mac, with nothing
5
- run on Windows — and first built against 2.16.1 on 2026-09-18, on Windows 11
6
- build 26200 with a GTX 1080 Ti. What that first session settled:
7
-
8
- - **The threading model holds.** A `setInterval` kept its cadence through a
9
- twelve-second run with a live window: 46 ticks against ~48 due. §"Threads
10
- and the event loop" shape 3 is the design, and it is no longer a claim.
11
- - **The surface model holds.** `BeginDraw` per damage rect, `Commit` per
12
- frame, over a virtual surface, with `src/backend/context2d.js` driving the
13
- Direct2D verb table unchanged — macos.md's split paying for itself.
14
- - **DirectWrite answers the engine contract**, `DetermineMinWidth` included.
15
- - `examples/simple.jsx`, `widgets.jsx`, `tasks.jsx`, `form/` and
16
- `dashboard.jsx` render, and synthetic input reaches React handlers.
17
-
18
- Since then: the desktop services (clipboard — text only —, drag and drop
19
- both ways, dialogs, tray, notifications), `<popup>`, input methods on IMM32,
20
- and the headless suite under `test/win32/` against a fake bridge.
21
-
22
- Since then also: `<Frame>` panes over a shared composition surface, and the
23
- **compositor frame clock**, which was the last thing pacing frames off a JS
24
- timer — on a 180Hz display that timer delivered 34.8 frames a second in an
25
- alternating 16/31ms rhythm, and nothing else about a frame mattered until it
26
- was gone (§"The frame clock").
27
-
28
- Still open: **live per-window scale** (the renderer cannot re-scale a window
29
- that is already up), and UI Automation's **text ranges**, without which a
30
- screen reader reads a field but cannot move through it. §"The plan" tracks
31
- it.
32
-
33
- The line counts below are measurements from 2026-09-11. Every claim about
34
- Windows not marked as measured comes from Microsoft's documentation or from
35
- shipping source code — Chromium, Firefox, Electron, Flutter, winit, Qt, SDL
36
- — and the sources are listed in §References. Where the sources left a
37
- question open it is marked unverified rather than guessed, and the claims
38
- the design leans on hardest are gathered in §"What to probe first".
39
-
40
- It started from an architecture brief for "a modern Win32 toolkit
41
- backend": Win32 windows, Direct3D 11 with a flip-model swapchain under
42
- DirectComposition, Direct2D and DirectWrite, `WM_POINTER`, TSF, per-monitor
43
- DPI, UI Automation, OLE. Most of it is right for this project. Some of its
44
- defaults are right for a toolkit that owns its process and wrong for an
45
- addon loaded into someone else's `node.exe`, and one of them — a
46
- `FLIP_DISCARD` swapchain as the window's content — would give up the part
47
- of the architecture this project has spent the most work on. §"The brief,
48
- cross-checked" goes through it point by point.
49
-
50
- ## What this is, and is not
51
-
52
- This is the plan for the **fourth backend family**, and the step that makes
53
- react-x11 cross-platform rather than a Linux-and-Mac project: X11, Cocoa and
54
- Wayland ship — Wayland landed in #548 three days after this document was
55
- written, which is what made `src/backend/` due — and this is Windows. Like both of those it is **a backend beside the others, not a
56
- migration**. X11 stays the remote answer ([remote.md](remote.md)) and the
57
- window-manager story; the Cocoa path is untouched; nothing below changes
58
- how either works — except to lift the pieces of `src/cocoa/` that were
59
- always backend-neutral into a shared home, which macos.md §"The split"
60
- already scheduled for "when a second native backend or Wayland actually
61
- starts".
62
-
63
- The goals are the project's standing ones:
64
-
65
- - **a small, portable codebase.** Windows is macOS's case of the
66
- native-module clause, not Wayland's: there is no wire protocol to speak,
67
- only DLLs and COM interfaces, so the bridge is a compiled addon by
68
- necessity. What keeps the rule's spirit is what kept it on macOS — the
69
- addon is **thin and mechanism-only** (§"The bridge");
70
- - **the whole range of desktop UI** the platform permits — applications,
71
- utilities, tray tools;
72
- - **the best experience available per system** — input-to-photon first,
73
- then stability, then client CPU and memory.
74
-
75
- The walls, up front:
76
-
77
- - **A window manager cannot be written.** DWM is the compositor and every
78
- top-level frame is its body; there is no substructure redirect and no
79
- reparenting of other programs' frames. `examples/wm.jsx` stays X11.
80
- (Shell-adjacent tools that move and size other programs' windows — the
81
- tiling helpers — can be written against `SetWindowPos`, except against
82
- elevated windows; nothing here plans one.)
83
- - **No network transparency.** Remote Desktop remotes the whole session and
84
- an app has no say in how. The X11 backend remains the answer when the
85
- display is elsewhere.
86
- - **WinUI 3 and the Windows App SDK are not the substrate.** They are a
87
- widget framework — the Windows analogue of what this project _is_, not
88
- of what it rides on. Their relevance is interop, later: hosting XAML
89
- content, or being hosted, through Content Islands.
90
- - **Embedding bends rather than breaks.** Unlike macOS and Wayland, Windows
91
- has cross-process child windows — Chromium parents its own GPU process's
92
- window that way — so `<foreign>` keeps a rung, though not XEmbed's focus
93
- protocol (§"Windowing semantics").
94
-
95
- ## What Windows actually changes
96
-
97
- X11 is "the client encodes drawing, the server rasterizes". Wayland is "the
98
- client rasterizes, the compositor composites". macOS is "the client mutates
99
- a retained layer tree, the render server composites and animates it".
100
- Windows is a fourth shape: **the client rasterizes, into retained
101
- composition surfaces that DWM composites.** It sits between the two
102
- inversions [macos.md](macos.md) describes:
103
-
104
- - **Like Wayland, the pixels are ours.** There is no server-side drawing
105
- protocol to encode into. GDI still exists and is not worth using — no
106
- alpha, antialiasing that is not worth the name, CPU only — so the drawing
107
- is Direct2D on the app's own Direct3D device, which is the Wayland plan's
108
- Tier D shape with a rasterizer the platform maintains instead of one of
109
- ours.
110
- - **Like macOS, the compositor keeps a retained tree.** DirectComposition
111
- gives an app a tree of visuals that DWM composites, and — the part that
112
- matters most here — a DirectComposition **surface keeps its pixels
113
- between frames**, takes an update of any rectangle of itself
114
- (`IDCompositionSurface::BeginDraw` with an update rect: every pixel inside
115
- is redrawn, every pixel outside is kept), and **scrolls its own
116
- contents** (`IDCompositionSurface::Scroll`). That is the X11 performance
117
- architecture verbatim: damage rects become `BeginDraw` rects, the scroll
118
- blit becomes one `Scroll`, the paint cache and the cull are untouched —
119
- and, unlike the Cocoa swapchain, no catch-up copy is ours to owe, because
120
- the retention is DirectComposition's job, done on the GPU.
121
-
122
- Two more differences decide the rest of this document:
123
-
124
- - **A window belongs to the thread that created it, not to the main thread.**
125
- AppKit insists on the process's main thread, so the Cocoa backend pumps
126
- `NSApplication` from a Node timer, and AppKit's modal loops — a live resize,
127
- menu tracking, a drag session — freeze Node's event loop for as long as they
128
- run; macos.md §"Input and the run loop" called that the one architectural
129
- risk in its plan. That pump is now the fallback: the default on macOS moves
130
- the JS to a worker and leaves AppKit the main thread (macos.md §"JS on a
131
- worker"). Win32 has the same modal loops and more of them — `DoDragDrop`
132
- does not return until the drop — but it lets the addon run them on a thread
133
- of its own, so JS never sits inside one (§"Threads and the event loop"). It
134
- is the largest structural difference from the Cocoa backend, and it is in
135
- Windows' favour.
136
- - **Fractional scale is the normal case, and it changes under a window.**
137
- 125%, 150% and 175% are what laptops ship at, and a laptop beside an
138
- external monitor is two scales on one desk. The four-generation ladder in
139
- [scale.md](scale.md) collapses to one authoritative, per-monitor, live
140
- answer — the same collapse Wayland and macOS give — but "live" is the
141
- operative word: a window dragged to the other monitor is expected to re-lay
142
- itself out at that monitor's scale. That is the one renderer change this
143
- backend cannot ship without (§"Layout").
144
-
145
- And three things get easier:
146
-
147
- - **Photon time is observable — on Windows 11.** Like Wayland's
148
- presentation-time and unlike Core Animation, Windows 11's compositor
149
- reports per-display statistics — when a frame was presented, the vblank it
150
- met — and offers a clock to wait on. The project's first metric,
151
- input-to-photon, can be measured there rather than bracketed; Windows 10
152
- has only estimates, and brackets it as macOS does.
153
- - **Shortcuts survive a Cyrillic layout by themselves.** Windows reports
154
- letter keys as virtual-key codes, and the Russian layout gives the key in
155
- QWERTY's C position `VK_C` — the Latin-accelerator rule `keyboard.js`
156
- needed code for on X11 arrives with the event (§Input).
157
- - **The desktop is readable without asking.** No TCC, no portal: a desktop
158
- app can read the screen, enumerate windows and register a system-wide
159
- hotkey. The eyedropper, global shortcuts and window lists have a rung
160
- here that macOS does not give them, with integrity levels as Windows' own
161
- boundary instead.
162
-
163
- ## The brief, cross-checked
164
-
165
- The brief this started from, claim by claim, against what this renderer
166
- actually needs:
167
-
168
- | the brief says | for react-x11 | lands in |
169
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
170
- | Win32 is the baseline: `RegisterClassEx`, `CreateWindowEx`, `GetMessage`/`DispatchMessage`, a `WNDPROC`; everything newer sits on an HWND | **agree** | §The bridge |
171
- | D3D11 → flip-model swapchain (`FLIP_DISCARD`) → `CreateSwapChainForComposition` → a DirectComposition visual tree | **agree on DirectComposition; not `FLIP_DISCARD`.** Its buffers come back undefined and it rules out partial presentation, so every frame would repaint the whole window — the per-buffer cost wayland.md charged a GPU swapchain with. Two Windows objects keep the damage model instead: a `FLIP_SEQUENTIAL` swapchain, whose `Present1` takes dirty rects and a scroll rect and copies the rest of the last frame forward itself, and a DirectComposition surface, which retains its pixels, takes a `BeginDraw` per rect and scrolls itself. The surface is the lean, for how it resizes | §Rendering |
172
- | Direct2D + DirectWrite into the D3D surface; many toolkits take only DirectWrite and shape with HarfBuzz | **agree, and DirectWrite end to end** — matching, fallback, shaping, bidi, line breaking, hit testing — the way the Cocoa backend uses CoreText rather than fontkit | §Text |
173
- | Windows 11's composition swapchain (`IPresentationManager`) as an opt-in fast path | **later, and not for the 2D content**: it presents whole buffers, with no dirty rects and no scroll, and its present-at-a-time and per-buffer fences are video's and `<glarea>`'s concerns | §Rendering |
174
- | A waitable swapchain + `MsgWaitForMultipleObjectsEx` as the frame loop; paint from `WM_PAINT`/`WM_SIZE` too, because modal loops take over | **the loop shape is right for the UI thread; the frame signal to JS comes from the display's clock.** The modal-loop problem is solved by JS not being in the loop at all | §Threads and the event loop |
175
- | DirectComposition buys per-pixel alpha without layered windows, smooth resize, cheap transform and opacity animation, and surfaces without child HWNDs — tooltips and menus can be visuals rather than windows | **agree, except the last.** A visual cannot draw outside its window, and a menu or tooltip near the window's edge must, so `<popup>` stays a real window; everything that stays inside the window is a node already | §Windowing semantics |
176
- | `EnableMouseInPointer(TRUE)` + `WM_POINTER` for mouse, touch and pen through one path | **not from an addon.** It can be called once per process and never undone, and it changes mouse delivery for every window in `node.exe`, other addons' included. Chromium does not call it either: the mouse stays on `WM_MOUSE*`, and pen and touch arrive as `WM_POINTER` without the switch | §Input |
177
- | Raw Input for unaccelerated, high-frequency deltas | **not needed** — it earns its place in pointer lock, which the API does not have | — |
178
- | Direct Manipulation for precision-touchpad inertia and rubber-banding | **agree, later.** The wheel path scrolls correctly without it, on the high-precision wheel messages touchpads send by default | §Input |
179
- | TSF (`ITextStoreACP2`) rather than `WM_IME_*` | **TSF is the target, IMM32 the first rung.** The browsers use TSF; Qt, winit, Flutter and SDL use IMM32. TSF's synchronous text-store queries meet this design's threading at its hardest point | §IME |
180
- | Per-monitor-v2 DPI from the manifest or `SetProcessDpiAwarenessContext`; `WM_DPICHANGED`'s suggested rect used verbatim; `WM_GETDPISCALEDSIZE` | **per thread, not per process.** `node.exe`'s manifest declares no DPI awareness at all, and an addon must not set the process's: `SetThreadDpiAwarenessContext` on the addon's own UI thread gives its windows per-monitor-v2 whatever the process says. The messages: agree — and they need a renderer that can re-scale a live window | §Layout, §Threads and the event loop |
181
- | Custom titlebar: `WM_NCCALCSIZE` → 0, `WM_NCHITTEST`, `DwmExtendFrameIntoClientArea`; `DwmSetWindowAttribute` for dark mode, corners, Mica/Acrylic, caption colour | **agree**, with two additions. Windows 11's Snap Layouts flyout needs `WM_NCHITTEST` to answer `HTMAXBUTTON` over a drawn maximize button, and every answer is due synchronously on the UI thread, so the regions are pushed ahead from JS. And the attributes are build-gated: documented from Windows 11 (build 22000), the system backdrop from 22H2 (22621) | §Windowing semantics |
182
- | UI Automation, provider side, through `WM_GETOBJECT`; MSAA is dead | **agree on UIA**, which Microsoft recommends for new work and whose providers also answer MSAA clients. Where its calls arrive is the design question | §Accessibility |
183
- | OLE for the clipboard and drag and drop | **agree.** OLE wants a single-threaded apartment on a thread that keeps pumping messages, which the addon's own thread can guarantee and Node's main thread cannot | §Windowing semantics |
184
- | Ship a manifest: comctl32 v6, DPI awareness, `longPathAware`, `activeCodePage` UTF-8; build ARM64 beside x64 | **for a packaged app.** `node app.js` runs under `node.exe`'s manifest — `supportedOS` entries and nothing else — and the addon must work without any of it: comctl32 v6 is reachable from a DLL through an activation context (`ISOLATION_AWARE_ENABLED`, a manifest at resource 2), and UTF-8 is moot, because N-API hands the addon UTF-16 directly. ARM64 and x64 prebuilds: agree | §Packaging and identity |
185
- | WinUI 3 and the Windows App SDK (2.0 shipped April 2026) are a competitor; Content Islands for interop | **agree** — 2.0.1 shipped on 2026-04-29 | §What this is |
186
- | Skip GDI/GDI+, layered windows, `SetWindowRgn` | **agree for drawing.** GDI keeps two small jobs: reading the screen for the eyedropper, and `PrintWindow` for test snapshots | §The desktop around the app, §Testing |
187
- | HWND ≈ `NSWindow`, a DComp visual tree ≈ a CALayer tree, a DXGI flip swapchain ≈ a `CAMetalLayer` drawable; "if your Cocoa backend renders through Metal, D3D11 → composition swapchain → DComp visual ports cleanly" | **the mapping is right, the premise is not ours.** The Cocoa backend does not render through Metal: its default presenter is CoreGraphics into an IOSurface swapchain. The symmetric design is Direct2D into a DirectComposition surface. And CALayer's _property_ vocabulary — colours, radii, borders and shadows that animate in the render server — is closer to the WinRT visual layer than to classic DirectComposition, which decides where promotion lands | §Rendering |
188
- | Read Chromium's `ui/gfx/win` and viz DComp code, Ghostty's and Zed's Windows backends, GLFW's `win32_window.c` | **agree**, plus Firefox's DirectComposition compositor, Electron's `node_bindings_win.cc`, and the Flutter and winit Windows shells | §References |
189
-
190
- ## What already carries over
191
-
192
- Measured at `13ffb34`: `src/` is 71,814 lines of `.js`. The inventory
193
- [wayland.md](wayland.md) took at 2.2.1 still has the same shape: the
194
- reconciler, yoga, the style system, event synthesis, focus, text editing
195
- and selection, the a11y model, keysyms, compose, anchors and the D-Bus
196
- stack never name a display system, and `components/` (7,935), `frame/`
197
- (1,306) and `refresh/` (536) carry over whole. Of `src/nodes/` (15,610,
198
- split by concern in #536), the `window/` part (3,801) realizes against a
199
- window object whose shape is `src/testing/mock-app.js`'s — and the Cocoa
200
- backend is the proof that the shape can be met with no X anywhere
201
- underneath.
202
-
203
- The better estimate of the work is the Cocoa backend itself, because it is
204
- the same kind of thing: `src/cocoa/` is **8,865 lines** implementing that
205
- contract plus fifteen platform services, over a bridge of **~9,800 lines**
206
- of Objective-C++ (`@windowkit/appkit` 0.9's `src/`) from which the backend
207
- calls about 140 natives. A Windows backend is that shape again. How much
208
- of the JS half comes free is the part worth planning, and `src/cocoa/` was
209
- written with the split in mind:
210
-
211
- | in `src/cocoa/` today | lines | on Windows |
212
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
213
- | `context2d.js` — the canvas-shaped context over a verb table | 1,301 | **shared.** The verb table becomes the contract two bridges implement, as macos.md §"The split" proposed. CoreGraphics' own trade-offs (stroke chunking, gradient-stop normalization) become per-bridge capabilities |
214
- | `surface.js` — `app.createSurface` | 235 | **shared**, over the same verbs |
215
- | `promotion.js` — which nodes may promote, and the CALayer ops | 705 | the **policy** — the z-order test, the idle grace — shared; the ops per bridge |
216
- | `app.js` — the pump, the per-display frame clocks, event routing | 1,242 | the frame queue and the routing are a template; **the pump does not exist** (§Threads and the event loop) |
217
- | `window.js` — the ntk-shaped window and its IOSurface swapchain | 669 | the window half is a template; the swapchain does not transfer, because a DirectComposition surface retains |
218
- | `fonts.js` — CoreText behind the engine contract | 984 | the contract, and the `linebreak`-driven floor and eliding, shared; DirectWrite replaces CoreText |
219
- | `presenter.js` — the layer presenter | 1,250 | Core Animation's; a Windows retained presenter is later work behind the same hooks |
220
- | `keymap.js` and the service modules — `dnd`, `filepanels`, `notifications`, `statusitem`, `dock`, `permissions`, `calendar`, `screencolor`, `glarea`, `globalmenu`, `bezels`, `panehost`/`panewindow`, `native` | ~2,480 | per-backend rungs behind the same hooks |
221
-
222
- Outside `src/cocoa/`, seventeen `process.platform` checks decide darwin
223
- against Linux (`filedialog.js`, `notifications.js`, `appearance.js`,
224
- `bus.js`, `permissions.js`, `ClickToComponent.js` and a few more); each
225
- grows a third answer. And the D-Bus stack loses nothing but has nothing to
226
- talk to: there is no session bus on Windows, so every D-Bus-first ladder
227
- ([dbus.md](dbus.md)) lands on its "there is no bus" floor — built
228
- first-class for exactly this — and Windows supplies its own rungs above it
229
- (§"The desktop around the app").
230
-
231
- ## Layout: nothing to port, one thing to build
232
-
233
- yoga runs unchanged: layout is platform-free, and it already runs in
234
- device pixels with whole-pixel rounding ([scale.md](scale.md)). Two
235
- consequences of Windows' scale model are routine rather than new.
236
- Fractional scale is the common case, so scale.md's caveat — a
237
- one-logical-pixel hairline at 1.25× lands on one device pixel or two,
238
- depending on where it falls — is what most Windows users will see. And
239
- DirectWrite measures in fractional DIPs, which the text engine rounds up to
240
- whole device pixels at the same boundary the CoreText engine does, because
241
- a fractional measure is what makes a yoga layout blow up.
242
-
243
- The thing to build is a **live, per-window scale**. `useScale()` is
244
- "static for the life of the root" (scale.md): one number, chosen at
245
- creation, on every backend. That is right for X11, which has one
246
- coordinate space and no per-window scale protocol, and where re-scaling
247
- mid-drag is a resize the window manager fights. Windows is the opposite
248
- case. A laptop at 150% beside a monitor at 100% is the ordinary desk; a
249
- per-monitor-v2 window is expected to re-lay itself out when it crosses
250
- between them; and Windows says when, with `WM_DPICHANGED` and the
251
- rectangle it suggests at the new scale — and before it
252
- `WM_GETDPISCALEDSIZE` (Windows 10 1703 and later), in which the window may
253
- answer the size it wants there. A window that ignores both is not blurry,
254
- because Windows does not stretch a per-monitor-aware window; it is simply
255
- the wrong physical size on the other monitor.
256
-
257
- Until the renderer can re-scale a live window — a relayout, every
258
- scale-keyed cache invalidated, text re-measured, the window's own
259
- geometry converted — the backend's interim answer is to take the suggested
260
- rectangle and scale the content visual by the ratio, which is what Windows
261
- itself does to a window that is only system-DPI aware: the right physical
262
- size at once, soft until the next frame at the new scale. The renderer
263
- half is backend-neutral: Wayland's fractional-scale protocol asks for the
264
- same thing ([wayland.md](wayland.md)), and a Cocoa window dragged from a
265
- retina panel to a standard monitor would use it too.
266
-
267
- ## The bridge: our own, and why
268
-
269
- ### What it has to provide
270
-
271
- The contract is the Cocoa one with Windows mechanisms under it; macos.md
272
- §"The split" already named its pieces — a verb table over an opaque
273
- surface handle, surface operations, text natives, windows, events, and the
274
- platform services. Grouped:
275
-
276
- - **App and threads** — start the UI thread; the event channel; the
277
- command queue; the state the UI thread publishes (window rects, DPI,
278
- monitors, keyboard locks); shutdown.
279
- - **Windows** — create (overlapped, popup or tool; owner, topmost,
280
- no-activate, transparent), show and hide, title, geometry in physical
281
- pixels, minimum and maximum size, maximize, minimize, fullscreen and
282
- restore, flashing, DWM attributes, per-window hit-test regions and
283
- cursor, the window's AppUserModelID, relaunch properties and icon.
284
- Events: geometry, including live-resize ticks bracketed by
285
- `WM_ENTERSIZEMOVE` and `WM_EXITSIZEMOVE`; DPI changes with the suggested
286
- rect; close requests; activation; display changes.
287
- - **Composition** — one device (Direct3D 11, Direct2D, DirectComposition)
288
- on the JS thread; a target and a root visual per window; the window's
289
- surface (`BeginDraw`/`EndDraw` per rect, `Scroll`, resize); child visuals
290
- for promotion, `<glarea>` and element-owned surfaces; `Commit`; frame
291
- statistics.
292
- - **Drawing** — the `ctx*` verb table over a Direct2D device context, and
293
- the surface operations under the names `@windowkit/appkit` already
294
- exports (`createSurface`, `surfaceSize`, `scrollSurface`,
295
- `copySurfaceRegion`, `ctxDrawSurface`, `blitSurface`, `ctxGetImageData`,
296
- `ctxPutImageData`), so one context wrapper drives both bridges.
297
- - **Text** — DirectWrite layouts built from span lists, their metrics, hit
298
- tests and drawing; font matching and fallback; the glyph-run natives of
299
- #432 (glyph for a code point, advances, shaping, drawing a run); fonts
300
- loaded from bytes.
301
- - **Input** — mouse, wheel and pointer; keys joined to the characters they
302
- typed; IME composition; cursors.
303
- - **Services** — the clipboard (formats, delayed rendering, a change
304
- listener); OLE drag and drop both ways, with drag images; the Common
305
- Item Dialog; the tray icon and popup menus; the taskbar (overlay badge,
306
- progress, flashing, jump lists); toasts; appearance (`UISettings`, high
307
- contrast); screen reading for the eyedropper; global hotkeys; UI
308
- Automation.
309
- - **Testing hooks** — synthetic input posted into the window procedure; a
310
- snapshot through `PrintWindow`.
311
-
312
- ### What npm has
313
-
314
- Surveyed 2026-09-11 — versions and dates from the npm registry, the
315
- message-loop column from each project's source — with one question put to
316
- every candidate: could it be the substrate (windows, input and
317
- presentation) under a renderer that keeps its own layout, its own event
318
- synthesis, its own 2D dialect and its own text engine?
319
-
320
- | package, version (last publish) | what it is | how it meets the message loop | fit |
321
- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
322
- | `koffi` 3.2.1 (2026-09) | FFI over N-API — structs, unions, `__stdcall`, raw function pointers and so COM vtables; prebuilt for x64, ARM64 and ia32 | the caller's; callbacks from other threads are queued to the JS thread, which its documentation warns can deadlock | **the spike**, and leaf calls |
323
- | `ffi-rs` 1.3.7 (2026-08) | FFI over napi-rs, threadsafe async callbacks | the caller's | a spike alternative |
324
- | `bun:ffi` | Bun's built-in FFI | its threadsafe callbacks are marked experimental, not for production | leaf calls under Bun |
325
- | `libwin32` 0.12 (2026-06), `win32-api` 26.1 (2025-02) | Win32 wrappers and signature catalogs over `koffi` | `libwin32`'s window demo runs a blocking `GetMessage` loop on the JS thread, so Node's own loop never runs | reference |
326
- | `@bun-win32/*` 2.0 (2026-06) | Bun FFI bindings including Direct3D 11, Direct2D, DirectWrite and DirectComposition, reaching COM through vtable pointers | no message-loop story | reference, Bun only |
327
- | `@kmamal/sdl` 0.11 (2025-08) | SDL 2 over N-API, with WebGL and WebGPU siblings | polled from JS by `setInterval(poll, 0)`; a live resize blocks it, and the SDL issue is closed as won't-fix | no — the Cocoa pump's costs, CPU blits, x64 only |
328
- | `@node-3d/glfw` 7.3 (2026-08), which replaces `glfw-raub` | GLFW over N-API | an idle pump on the JS thread; GLFW documents that a move, a resize or a menu blocks event processing | no — OpenGL only |
329
- | `skia-canvas` 3.0 (2025-09) | Skia with `Window` and `App` over winit, Vulkan on Windows | by default winit's loop runs on the Node thread until the last window closes; a `'node'` mode pumps it every few milliseconds instead | no — its own drawing API and Skia's text displace ntk's dialect and the engine contract: wayland.md's Tier C verdict again |
330
- | `@nodegui/nodegui` 0.74 (2026-05) | Qt 6 widgets | through `qode`, a forked Node binary: a helper thread waits on libuv and posts to the Qt thread, which runs `uv_run` — Electron's code, transplanted | no — a widget toolkit and a runtime of its own |
331
- | `node-gtk` 4.1 (2026-07) | GObject bindings, GTK 4 on Windows | GLib's loop runs libuv from a GLib source; on Windows the I/O half is marked FIXME | no |
332
- | `webgpu` 0.6 (2026-08) | Dawn for Node | cannot present to a window | no |
333
- | `winpane` 0.1 (2026-03) | napi-rs overlay surfaces over Direct3D 11, Direct2D, DirectComposition and DirectWrite | **a dedicated thread owns every window and GPU object in its own `GetMessageW` loop**; commands reach it on a channel with a `PostMessageW` wake | reference — pre-1.0, with this document's threading shape |
334
- | `node-api-dotnet` 0.9 (2026-09) | Microsoft's JS↔.NET bridge, which reaches WinRT and the Windows App SDK | .NET's; JS objects only on the JS thread | no for the core — a public preview that puts .NET (and, for notifications, the Windows App SDK runtime) under every app |
335
- | NodeRT, `ffi-napi`, `libui-node`, `proton-native`, `react-nodegui` | WinRT projections, the old FFI, libui bindings, React over libui and Qt | — | unmaintained — the React renderers among them wrapped someone else's widget toolkit |
336
- | the leaf packages: `clipboardy`, `@napi-rs/clipboard`, `systray2`, `trayicon`, `node-notifier`, `powertoast`, … | clipboard, tray, notification and dialog integrations | PowerShell or a helper `.exe` per call, a polling loop, or data written eagerly | no — none carries a lazy payload, and each is a few dozen lines inside the addon |
337
- | AccessKit — Windows adapter 0.35, C bindings 0.23 (2026-09) | accessibility infrastructure in Rust: the app pushes a tree, the adapter serves UI Automation from it, text ranges for single- and multi-line inputs included, rich text not yet | installed from the UI thread; answers from its own copy | **the reuse candidate** (§Accessibility) |
338
- | `robotjs` 0.9 (2026-08) | input injection | — | leaf, for the tests that want the real input stack |
339
-
340
- None of them is a substrate. The windowing candidates are policy layers,
341
- and each owns something this renderer must keep. Every one that polls the
342
- OS from the JS thread — SDL, GLFW, winit in either of `skia-canvas`'s modes
343
- — freezes JS in Windows' modal loops, which is the Cocoa backend's
344
- standing cost; `skia-canvas` also owns the drawing API and the text stack,
345
- and NodeGui the widget set and the Node binary. Nothing carries lazy
346
- clipboard payloads, drag and drop in both directions, the Common Item
347
- Dialog or UI Automation, and nothing but a pre-1.0 overlay library reaches
348
- DirectComposition, Direct2D and DirectWrite at all — which is where this
349
- backend's performance and its text live.
350
-
351
- The prior art worth reading is shapes rather than packages: `winpane`'s
352
- dedicated thread; NodeGui's loop merge, as the thing not to need;
353
- react-native-windows, which keeps JS on a thread of its own and posts every
354
- mount to the UI thread that alone touches its composition visuals; and the
355
- small addons that already run a message loop on a thread of their own
356
- inside stock `node.exe` — `node-usb-detection`'s listener window,
357
- `uiohook-napi`'s hook thread feeding a threadsafe function.
358
-
359
- ### The decision
360
-
361
- **Our own addon — `@windowkit/win32`, `@windowkit/appkit`'s sibling —
362
- mechanism only**: verbs, handles and events, no policy, no widget
363
- logic and no JS canvas class inside it, the same export-list discipline
364
- macos.md §"The split" set for the Cocoa bridge. It ships as an
365
- `optionalDependency` with `os: ["win32"]` and prebuilds for x64 and ARM64,
366
- over N-API so Bun loads it too; absent on every other platform, the way
367
- `@windowkit/appkit` is absent on Linux.
368
-
369
- Reuse where it pays: `koffi` is the fastest route to Phase 0's first
370
- window, and is thrown away after it; AccessKit was the lean for UI
371
- Automation, through its C bindings, until a provider of our own was built
372
- instead (§Accessibility); and `robotjs` injects real input for
373
- the tests that want the whole input stack.
374
-
375
- ### C++, and where Rust would win
376
-
377
- **C++ with node-addon-api**, the toolchain `@windowkit/appkit` already
378
- uses: one build and publish pipeline (`node-gyp`, prebuilds in the
379
- package, the same install script); the Windows SDK's own headers — WRL and
380
- C++/WinRT — are all it needs for COM and WinRT; and every reference
381
- implementation worth reading (Chromium, Firefox, Electron, the Flutter
382
- engine, GLFW) is C or C++ and ports line for line. AccessKit is Rust but
383
- ships C bindings, so adopting it does not force the question.
384
-
385
- Rust — napi-rs over `windows-rs` — is the recorded alternative, and its
386
- advantages are real rather than fashionable: `#[implement]` makes the COM
387
- objects this bridge has to implement (drop target, drop source, data
388
- object, TSF's text store) safer to write, and `cargo-xwin` cross-compiles
389
- the MSVC target from a Mac — the maintainer's machine — where C++ needs a
390
- clang-cl setup to do the same. One language family across the windowkit
391
- bridges wins until the cross-compile matters more than the symmetry.
3
+ The `win32` backend draws a react-x11 app into real Win32 windows: one
4
+ DirectComposition surface per window, Direct2D for drawing, DirectWrite for
5
+ text. It is **the default on Windows**, and it needs the `@windowkit/win32`
6
+ native bridge; without the bridge, `'auto'` falls back to X11. The examples
7
+ run on it, except `wm` and `xeyes`, which are X11 window-manager demos.
8
+
9
+ [windows-integrations.md](windows-integrations.md) is the row-by-row status of
10
+ what an app can reach on Windows. Why the backend is built the way it is — the
11
+ threading model, the rendering internals, the plan and its open questions — is
12
+ the design record: [design/windows.md](../design/windows.md).
13
+
14
+ ## Choosing a backend
15
+
16
+ `createRoot({ backend })`, or `REACT_X11_BACKEND` without touching code:
17
+
18
+ | asked | on Windows |
19
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `'auto'` (the default) | `'win32'` when `@windowkit/win32` loads. Otherwise X11 through `DISPLAY`, with one console warning outside production, so a WSLg or Cygwin setup keeps working |
21
+ | `'win32'` | the bridge is required: if it cannot load, `createRoot()` rejects with an error that lists what it tried and how to install it |
22
+ | `'x11'` | an X server through `DISPLAY`. Also what `'auto'` picks when you pass `display` or `stream`. Windows has no `fc-match`, so tell ntk where the fonts are with `fontSource` or `REACT_X11_FONT_SOURCE` |
23
+
24
+ ```jsx
25
+ const root = await createRoot(); // win32 on Windows, x11 if the bridge is missing
26
+ const root = await createRoot({ backend: 'win32' }); // the bridge or an error
27
+ ```
28
+
29
+ Asking for `'win32'` on any other platform throws: the backend is
30
+ Windows-only.
31
+
32
+ ## Installing the bridge
33
+
34
+ - `@windowkit/win32` is an `optionalDependency` of react-x11 with
35
+ `os: ["win32"]`, so `npm install react-x11` on Windows installs it and
36
+ every other platform skips it. If optional dependencies were omitted,
37
+ install it yourself: `npm install @windowkit/win32`.
38
+ - Prebuilds ship for **x64 and ARM64**. Building from source needs Visual
39
+ Studio with the "Desktop development with C++" workload.
40
+ - It is an N-API addon, so it loads under Bun (`bun.exe`) as well as Node.
41
+ - `REACT_X11_WIN32_PATH=<path>` loads the bridge from a path instead of the
42
+ installed package: a checkout, or the file shipped beside a compiled
43
+ executable (see [Packaging and identity](#packaging-and-identity)).
44
+ - An older bridge degrades feature by feature rather than failing: a missing
45
+ verb answers "unsupported" (for example, `useWindowState()` sees `false`,
46
+ and a `<glarea>`'s children are not drawn).
47
+ - Built and measured on Windows 11. Windows 10 is not a verified target.
392
48
 
393
49
  ## Threads and the event loop
394
50
 
395
- ### What the Cocoa backend does, and what it costs
396
-
397
- AppKit must run on the process's main thread, which in a Node process is
398
- the thread that runs JS. So the Cocoa backend does what macos.md §"Input
399
- and the run loop" called option 1: Node's loop is the master, and a timer
400
- pumps AppKit every 8 ms (`native.pump2()`, `src/cocoa/app.js`). That is
401
- pump mode, which since threaded mode became the default runs only with
402
- `REACT_X11_THREADED=0` or where the move to a worker declines (macos.md
403
- §"The renderer's half"). Two costs follow, and both are measured:
404
-
405
- - **Input waits for the pump.** An event that lands just after a tick
406
- waits most of a pump interval before JS sees it. Frames got a timer of
407
- their own to escape the quantization (macos.md §"Measured: incremental
408
- floors, the strip, the frame timer"); input did not.
409
- - **A modal loop freezes Node.** AppKit runs live resize, menu tracking and
410
- drag sessions in loops of its own _inside_ the pump call. With a drag up,
411
- a 25 ms `setInterval` did not fire for the drag's whole duration — 31
412
- seconds in the probe — and a microtask queued inside a bridge callback did
413
- not drain until the pump returned
414
- ([#484](https://github.com/sidorares/react-x11/pull/484)). The backend
415
- lives with it by doing a frame's work inside the callbacks that do arrive
416
- (`CocoaApp._afterInput`: `flushSyncWork`, `flushPendingFrames`,
417
- present), which is why a live resize re-lays out and a drag preview
418
- follows the pointer. But a timer, a socket read, an `await` or a React
419
- update from anything other than that callback waits for the gesture to
420
- end.
421
-
422
- ### Three shapes on Windows
423
-
424
- 1. **The Cocoa shape**: create the windows on Node's main thread and pump
425
- `PeekMessage` from a libuv timer. It works, and it inherits both costs,
426
- worse. The pump cannot become a wait: on Windows libuv blocks in
427
- `GetQueuedCompletionStatusEx` on its completion port (`uv_backend_fd`
428
- is -1 there), only a packet posted to that port wakes it, and a thread's
429
- message queue can be waited on only by `MsgWaitForMultipleObjectsEx` on
430
- that same thread — so no single call wakes for both, and a window on
431
- Node's thread can only be polled. And Windows' modal loops are more
432
- numerous and more total than AppKit's: `DoDragDrop` does not return
433
- until the drop — a drop into another program included — and neither do
434
- `TrackPopupMenuEx`, menu-bar tracking, `IFileDialog::Show` or a
435
- `MessageBox`. Every drag, menu and dialog would freeze JS. Every npm
436
- windowing package above that polls from the JS thread has exactly this
437
- behaviour.
438
- 2. **The Electron shape**: merge the loops. Electron runs libuv inside
439
- Chromium's Win32 message loop: a helper thread blocks on libuv's
440
- completion port with libuv's own timeout, gives each completion packet
441
- back to the port, and wakes the main thread to run one non-blocking
442
- `uv_run` (`shell/common/node_bindings_win.cc`, over a patched libuv).
443
- That needs whoever owns `main()` to own the outer loop. In stock Node
444
- the main thread sits in `uv_run` and an addon only ever runs as a
445
- callback inside it, and `uv_run` is documented as not re-entrant — the
446
- same reason macos.md held its option 3, "full inversion", in reserve.
447
- NodeGui does it on Windows with `qode`, a forked Node binary, which is
448
- what the shape costs when you do not own `main()`.
449
- 3. **A UI thread of the addon's own** — the recommendation. Win32 binds a
450
- window to the thread that created it, not to the main thread. So the
451
- addon starts one thread, and that thread creates every HWND, owns the
452
- OLE apartment and the DPI context, and runs a plain
453
- `GetMessage`/`DispatchMessage` loop; JS stays on Node's main thread.
454
- Events cross through a `napi_threadsafe_function` — `uv_async_send`
455
- underneath, a packet posted to libuv's completion port — so they wake
456
- Node's loop at once, from any thread, without waiting for a tick; each
457
- delivery runs in a callback scope that drains microtasks and next-ticks
458
- when it closes, so a handler's `setState` commits on the event that
459
- caused it — the #484 problem does not exist here. And every modal loop
460
- runs on the UI thread, where Windows expects it, while Node's loop keeps
461
- running: timers fire, sockets read, React commits and frames present
462
- during a live resize, with a menu open, and in the middle of a drag.
463
-
464
- Shape 3 is what macos.md's option 3 was for — JS never frozen by the
465
- platform's loops — without inverting anything, and it is available in this
466
- form only because Win32 permits it. AppKit does not, so macOS gets the same
467
- shape with the roles swapped — AppKit keeps the main thread and the JS moves
468
- to a worker — measured in
469
- [macos.md](macos.md#js-on-a-worker-a-ui-thread-of-the-bridges-own). It is
470
- also how the browsers split the work, and
471
- DirectComposition is documented to allow it: its objects are not bound to a
472
- thread, and a target needs only that the window belong to the calling
473
- process. Chromium's GPU process creates its window on a dedicated "window
474
- owner" thread and builds and commits the DirectComposition tree for that
475
- window from its GPU thread (`ui/gl/child_window_win.cc`,
476
- `ui/gl/dc_layer_tree.cc`).
477
-
478
- ### Who owns what
479
-
480
- | thread | owns | reaches the others by |
481
- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
482
- | Node's main thread | React, layout, the node tree; painting through Direct2D; the content's DirectComposition device and its `Commit`; DirectWrite | commands posted to the UI thread, one wake per frame; reading what the UI thread publishes |
483
- | the UI thread, one per process | every HWND and its message loop; the modal loops; the OLE apartment; the per-monitor-v2 DPI context; IME; the clipboard's owner window; the tray icon; hotkeys; the background plate (§Resize) | batched events through a threadsafe function per Node environment; state it publishes under a lock |
484
- | the frame thread | waiting on the display's clock | a tick through the same threadsafe function |
485
- | UI Automation's threads | calls into the accessibility providers | answered from the bridge's copy of the tree, which JS pushes (§Accessibility) |
486
-
487
- ### The rules that keep it from deadlocking
488
-
489
- Two threads that can each wait on the other eventually will. The design is
490
- three rules:
491
-
492
- - **JS never waits on the UI thread.** Every JS→UI call is a command on a
493
- queue — create, show, move, set the title, set the cursor, push hit
494
- regions — batched and sent with one wake per frame, the way one
495
- `CATransaction` carries a frame on Cocoa. A window is created
496
- asynchronously: the command returns a handle at once, and the first frame
497
- targets it when the UI thread reports it exists. Anything JS reads back
498
- synchronously — window rects, monitors, the keyboard's lock state — is a
499
- value the UI thread _publishes_ into lock-guarded memory and JS reads
500
- without a hop. That is also the only correct place to get those values:
501
- Node's thread is DPI-unaware, and a monitor or cursor query made there
502
- answers in scaled-down virtual pixels. The contract makes this natural:
503
- the X11 app and window contract was asynchronous from the start, because
504
- a display server sat at the far end of a socket — clipboard reads,
505
- dialogs and snapshots are promises, geometry arrives as events. The Cocoa
506
- bridge made much of it synchronous because it could; Windows puts the far
507
- end back, a thread instead of a socket, and the X11-shaped contract fits.
508
- - **What Windows asks synchronously, and JS can know in advance, is pushed
509
- ahead.** `WM_NCHITTEST` (every mouse move over the window),
510
- `WM_SETCURSOR`, `WM_GETMINMAXINFO`, the IME's candidate position and a
511
- drag's `DragOver` effect all need an answer before the window procedure
512
- returns. So JS pushes them after each commit — the hit regions of a drawn
513
- titlebar, the cursor of the hovered node, the size limits, the caret
514
- rect, the effect the last `onDragOver` chose — and the UI thread answers
515
- from what it holds. That is Electron's `app-region: drag` generalized,
516
- and it is how the X11 path already treats state the server needs without
517
- asking.
518
- - **The UI thread may wait on JS — bounded, message-aware, and only where
519
- JS alone has the answer**: a lazy clipboard payload (`WM_RENDERFORMAT`),
520
- a drag's `IDataObject::GetData`, and the resize handshake below. Each
521
- wait has a deadline and a fallback answer, and waits in
522
- `MsgWaitForMultipleObjectsEx` so that sent messages keep being
523
- dispatched meanwhile. Because JS never waits on the UI thread, the cycle
524
- that would deadlock cannot form.
525
-
526
- Two mechanics make the rules hold inside a modal loop, where the UI
527
- thread's own loop is not the one running. The command queue's wake is a
528
- message posted to a message-only window, whose procedure drains the queue:
529
- a thread message (`PostThreadMessage`) would be eaten by the modal loop,
530
- and a wait of our own does not run inside one. And the event channel
531
- carries batches: the UI thread appends to a queue and makes one
532
- threadsafe call per wake, consecutive motion folds to its latest position
533
- before it crosses, and JS drains the batch in one callback — so a burst of
534
- input is one macrotask rather than one microtask checkpoint and one commit
535
- per message.
51
+ The bridge runs every window on a UI thread of its own, and your JS stays on
52
+ Node's main thread. Windows' modal loops — a live resize, a native menu, a
53
+ drag (`DoDragDrop` does not return until the drop), a file dialog — run on
54
+ that UI thread, so **JS is never frozen by one**: timers fire, sockets read,
55
+ React commits and frames present while the user drags or a dialog is open.
56
+ This is the main behavioural difference from the Cocoa backend's pump mode.
536
57
 
537
- Drawing happens on the JS thread: the Direct3D device, the Direct2D
538
- context and the DirectComposition device are created and used there, and
539
- the UI thread never touches them. That matters beyond tidiness, because a
540
- DirectComposition `Commit` submits everything pending on its device from
541
- every thread — a device shared between two threads would commit one
542
- thread's half-made changes with the other's frame. Where Windows ties an
543
- object to a thread — OLE's drop target registration, the clipboard's owner
544
- window, hotkeys, `WM_GETOBJECT` — it is the UI thread's. The
545
- per-monitor-v2 DPI context is set there with
546
- `SetThreadDpiAwarenessContext` before the first window, which is what makes
547
- its windows DPI-correct under a `node.exe` whose manifest says nothing
548
- about DPI.
549
-
550
- ### Resize: where the threads meet
551
-
552
- A live resize is the one place the two threads must agree within a frame.
553
- DWM moves the window's edge on the drag's own schedule; if the content
554
- arrives later, the user sees the gap. Every platform has a handshake for
555
- this — Wayland's `configure`/`ack_configure`, X11's
556
- `_NET_WM_SYNC_REQUEST` — and on Windows the addon makes one. On each
557
- `WM_SIZE` inside the size-move loop the UI thread publishes the new size,
558
- wakes JS, and waits — bounded, pumping messages as it waits — for JS to
559
- commit a frame at that size before it returns to Windows. It is what
560
- Flutter's Windows embedder does: its `WM_SIZE` handling waits up to
561
- 100 ms for a frame of the new size and drops frames of the old one
562
- (`flutter_windows_view.cc`). JS lays out, paints and commits exactly as it
563
- does for a Cocoa resize tick, with `liveResizing` set from
564
- `WM_ENTERSIZEMOVE`/`WM_EXITSIZEMOVE` so the content floors defer the way
565
- they do there (`_deferContentFloors`, `src/nodes/window/size.js`).
566
-
567
- The deadline will be missed on a big tree — the Cocoa `resize` cell
568
- measures 28 ms a tick at 3,662 nodes (macos.md §"Measured: the frame clock
569
- and the large tree") — so what shows when it is missed matters as much as
570
- the handshake. Three things keep it clean. The window's surface is a
571
- virtual surface, which keeps its content across a resize, so late content
572
- is the last frame at its old size rather than garbage — placed at one to
573
- one, where a swapchain would stretch it, which is why Flutter has to drop
574
- old frames and a surface need not. The UI thread owns a **background
575
- plate** of its own — the root visual, in the window's background colour,
576
- resized in the same `WM_SIZE` from a device the UI thread commits by
577
- itself, with the content's visuals from the JS thread's device beneath it
578
- in the tree — so a newly exposed edge shows the app's background rather
579
- than black. X11 has always done that half: it is the window's background
580
- pixel, which `WindowNode` sets and the server paints before the client
581
- can. And the `liveResizing` deferral keeps each tick's frame as cheap as the
582
- tree allows.
583
-
584
- ### The frame clock
585
-
586
- **Built** (windows/src/frameclock.cc). A thread of the addon's blocks on
587
- `DCompositionWaitForCompositorClock` and ticks JS through the threadsafe
588
- function; JS asks one tick at a time through an auto-reset event, so an idle
589
- application parks that thread and there is no polling anywhere in it.
590
-
591
- Why it was worth building first, measured through one harness on a 180Hz
592
- display:
593
-
594
- | | fps | gap p50 | what arrived |
595
- | ---------------- | ----- | ------- | ------------------------------- |
596
- | `setTimeout(16)` | 34.0 | 31.0ms | 31ms x233 · 16ms x30 · 32ms x24 |
597
- | compositor clock | 180.2 | 5.6ms | 6ms x245 · 5ms x54 |
598
-
599
- Node rounds a timer up to the next system tick, the default tick is 15.6ms,
600
- and a 16.67ms request therefore lands on the **second** one. So the old clock
601
- was not a slow 60Hz — it was 34Hz alternating between one tick and two, which
602
- is the rhythm the eye reads as judder. No amount of making frames cheaper
603
- touched it: the frames that led here cost 0.9ms each.
604
-
605
- Two details from shipping practice, and what this does about each. The wait
606
- is not tied to the primary monitor, which is why it is preferred over
607
- `IDXGIOutput::WaitForVBlank` on the output under the window (where Chromium's
608
- default vsync thread waits on the primary's anyway). And it can hang if the
609
- adapter goes away — Chromium answers that with a 100ms timeout; here the
610
- quit event is passed into the wait, so shutdown never blocks on it, and the
611
- _frame_ hazard is answered one level up. A clock that ticked in the last
612
- 50ms is trusted with the next frame, and JS watches the promised tick for
613
- 250ms; one that has not may be asleep, so a frame interval's timer races
614
- it. Either way the tick, however late, says the clock is awake again, and
615
- nothing is latched. It was: the first cut fell back to the timer for the
616
- rest of the session on one missed tick, and a request made after a quiet
617
- spell can wait 0.3–3s for its tick — 5 of 8 cold starts of the components'
618
- Flow bench on a 180Hz panel did — so an application that sat still for a
619
- moment at startup ran at the timer's 34 frames a second from then on. A
620
- clock that never ticks again paces frames exactly as the timer alone would.
621
- The frame queue above it is the Cocoa one: per-window
622
- intervals from the refresh rate of the monitor under the window
623
- (`frameIntervalFor`), a discrete input answered on the spot
624
- (`flushPendingFrames`), a window nobody can see owing nothing, and
625
- `frameRate`'s pacing on top
626
- ([architecture/frame-pacing.md](architecture/frame-pacing.md)). What
627
- Windows 11 adds is the far end: the compositor's target statistics say
628
- when a frame was presented on each display, so `frameInFlight` can be the
629
- compositor's answer rather than a guess from elapsed time, and
630
- input-to-photon becomes a measurement — the input's timestamp against the
631
- present time of the frame that answered it. Windows 10 has only an estimate
632
- of the next frame, and brackets it as Cocoa does.
633
-
634
- ### Lifetimes, workers and Bun
635
-
636
- - The event function is referenced while any window exists, so a window
637
- keeps Node alive the way the X socket does, and unreferenced when none
638
- does, so an app with no windows can exit.
639
- - Teardown has two doors, because `process.exit()` skips environment
640
- cleanup hooks: the cleanup hook for an environment that ends normally,
641
- and a `process.on('exit')` handler for one that does not. Either one ends
642
- any modal loop the UI thread is in (`EndMenu`, cancelling the drag,
643
- dismissing the dialog), destroys the windows, posts `WM_QUIT` and joins
644
- with a deadline — a thread still inside a modal loop when the process
645
- exits is killed holding whatever it held. An event that arrives after its
646
- environment is gone reaches the threadsafe function's callback with no
647
- environment, and is dropped.
648
- - One UI thread per process, one event function per Node environment. The
649
- addon can be loaded by several workers at once, and a window owned by a
650
- window on another thread attaches the two threads' input — which a single
651
- UI thread never has to reason about.
652
- - Bun implements N-API's threadsafe functions and exports them on Windows,
653
- and runs the Cocoa bridge today. Two teardown crashes in them were fixed
654
- in August 2026 (oven-sh/bun#37031, #37201) and a Windows-only panic has
655
- been reported (#16210), so Phase 0 runs the event path under `bun.exe`
656
- as well as `node.exe`: everything above depends on it.
58
+ Events reach JS as soon as they happen, without waiting for a timer tick, and
59
+ a handler's `setState` commits on the event that caused it.
657
60
 
658
61
  ## Rendering
659
62
 
660
- ### The window's content: a DirectComposition surface
661
-
662
- Each window gets a target, a root visual and one surface — a virtual
663
- surface at the window's physical size, `DXGI_FORMAT_B8G8R8A8_UNORM`,
664
- alpha-ignored for an ordinary window (which is also what lets its text be
665
- ClearType, §Text) and premultiplied for `transparent`. The surface
666
- presenter's contract then maps one call to one call:
667
-
668
- - **a frame** is one `BeginDraw`/`EndDraw` per damage rect, painting
669
- through the Direct2D context `BeginDraw` hands back — positioned in a
670
- texture DirectComposition manages, at an offset the context wrapper folds
671
- into its transform — and then one `Commit`. Every pixel inside the rect
672
- is repainted (its old contents are undefined, which the paint pass's
673
- clear already assumes) and every pixel outside it is kept. One surface
674
- per device can be mid-draw at a time, which a frame's sequential rects
675
- never contest;
676
- - **`scrollRegion(rect, dx, dy)`** is `Scroll` and returns `true` — the
677
- fast path the X11 and Cocoa windows take, with the exposed strip
678
- repainted by the frame's own damage;
679
- - **`damageRectCap`** is measured, starting from Cocoa's 16: a `BeginDraw`
680
- has a fixed cost, so the right number of rects is a fact about
681
- DirectComposition rather than a choice;
682
- - **a resize** is `IDCompositionVirtualSurface::Resize`, which keeps the
683
- pixels inside the new bounds, so a live-resize tick allocates nothing
684
- window-sized — the churn #442 measured on Cocoa (+572 MB during a 40-tick
685
- burst before the explicit release) has nothing to fix here — and late
686
- content is the last frame rather than garbage (§"Resize: where the
687
- threads meet");
688
- - **reading back** is the one thing a surface refuses: the app cannot read
689
- its pixels. What the Cocoa window answers from its IOSurface —
690
- `getImageData` over the window, a snapshot — is answered here by
691
- `PrintWindow` once the commit has completed, or by painting the tree once
692
- more into an offscreen surface. Both are rare paths: tests and captures;
693
- - **what does not transfer** from the Cocoa window: the IOSurface chain,
694
- the catch-up copies `noteFrameDamage` and `_takeBack` owe, and
695
- `_releaseBacking`.
696
-
697
- The brief's `FLIP_DISCARD` swapchain is the one Windows presentation object
698
- that cannot do this: its buffers come back undefined and partial
699
- presentation is ruled out, so every frame would repaint the window. A
700
- `FLIP_SEQUENTIAL` composition swapchain can: `Present1` carries the frame's
701
- dirty rects and a scroll rect, and DXGI copies the rest of the previous
702
- frame forward and performs the scroll blit itself — Firefox presents that
703
- way too. It is the recorded alternative, and it has one thing the surface
704
- lacks, a waitable object to pace by. The surface is the lean for how it
705
- resizes: `ResizeBuffers` discards what a swapchain holds, so a frame that
706
- is late during a live resize has nothing to show, where a resized virtual
707
- surface still has the last one. Firefox also paints its WebRender tiles
708
- into DirectComposition surfaces, a `BeginDraw` per dirty rect
709
- (`gfx/webrender_bindings/DCLayerTree.cpp`); neither browser calls `Scroll`,
710
- which is why it is on the probe list.
711
-
712
- ### The verb table on Direct2D
713
-
714
- | verb | Direct2D |
715
- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
716
- | `fillRect`, `fillRects`, `strokeRect`, `clearRect` | `FillRectangle`, `DrawRectangle`; `clearRect` is a `Clear` inside an axis-aligned clip |
717
- | paths — `moveTo` to `closePath`, `arc`, `ellipse`, curves — `fill`, `stroke`, `setLineDash`, `lineDashOffset` | an `ID2D1PathGeometry` per path; `FillGeometry` and `DrawGeometry` with an `ID2D1StrokeStyle1` for caps, joins and dashes |
718
- | `roundRect` | `FillRoundedRectangle` for one radius, a path for per-corner radii |
719
- | `clip` | `PushAxisAlignedClip` for a rect under an axis-aligned transform — the clip every paint pass sets; a layer with a geometric mask otherwise, which is the expensive one, built winding or alternate by the rule `clip` is handed (@windowkit/win32 0.0.7). The Cocoa context's rule, to track the clip as a rect only where it can be stated exactly, carries |
720
- | linear and radial gradients | `ID2D1LinearGradientBrush`, `ID2D1RadialGradientBrush` — radial the verb the Cocoa bridge has as `ctxFillRadialGradient` |
721
- | `createPattern` | an image brush with extend modes — the pattern fill `<Flow>`'s grid wants |
722
- | `drawImage`, `ctxDrawSurface`, `blitSurface`, `copySurfaceRegion` | `DrawBitmap` and `DrawImage`; `CopyFromBitmap` for the plain copies |
723
- | `putImageData`, `getImageData` on a surface | `CopyFromMemory` in; a CPU-readable bitmap and `Map` out — answered RGBA, as the context's contract says |
724
- | `globalAlpha`, `globalCompositeOperation` | brush or layer opacity; `SetPrimitiveBlend` covers `source-over`, `copy` and `lighter`; the other Porter-Duff operators composite through `DrawImage` from an intermediate bitmap, and `multiply` to `luminosity` through the blend effect over a copy of the destination. The vocabulary is the bridge's own and is feature-detected by assigning and reading back, as Cocoa's `ctxSetBlendMode` is |
725
- | shadows | baked into the paint cache, never a live effect — the #413 rule: a filter re-run on every composite is the same trap on a GPU as on an X server |
726
- | `drawGlyphs` | `DrawGlyphRun`, grouped by face and size |
727
-
728
- Two Direct2D facts to design around rather than discover. A device can be
729
- lost — a driver update, a GPU reset, a laptop switching adapters — and
730
- recovering is the resize path again: a new device, new surfaces, one full
731
- frame, which the backend's fresh-surface logic already knows how to run.
732
- And Windows always has a device to ask for: where there is no hardware,
733
- WARP — Direct3D's software rasterizer, part of the OS — runs Direct2D,
734
- which covers virtual machines without passthrough and CI runners; that
735
- DirectComposition presents from a WARP device is the last item on the probe
736
- list. The Wayland plan's `'2d-sw'` rasterizer, uploaded into a `BeginDraw`
737
- rect, remains the cross-backend pixel reference rather than a fallback
738
- Windows needs.
739
-
740
- ### Promotion, and a retained tier later
741
-
742
- On Cocoa, promotion lifts the few nodes that animate onto CALayers above
743
- the window's bitmap, where the render server animates `backgroundColor`,
744
- `borderColor`, `borderWidth` and `borderRadius` while JS is busy (macos.md
745
- §"Layer promotion"). The Windows analogue is a visual above the window's
746
- surface, and the question is which composition API holds it:
747
-
748
- - **Classic DirectComposition** (`dcomp.h`, the API everything above
749
- already uses) animates any scalar property of a visual — its offset, its
750
- transform, an effect group's opacity, a rectangle clip's edges and corner
751
- radii — with curves DWM evaluates. That covers movement, fades and a
752
- radius. It has no colour to animate, so a background or border transition
753
- stays on the frame clock, and a node promoted for movement is a raster in
754
- a surface of its own.
755
- - **The WinRT visual layer** (`Windows.UI.Composition`, reached from Win32
756
- through the desktop interop interfaces, Windows 10 1803 and later) is
757
- CALayer's vocabulary almost property for property — sprite visuals with
758
- colour and gradient brushes, drop shadows, rounded clips — and the
759
- properties that matter animate in the system compositor: a brush's
760
- colour, a shadow's blur, colour and offset, and on Windows 11 a clip's
761
- corner radii, with key-frame easing that `EASING_CONTROL_POINTS` already
762
- expresses as cubic beziers. It also has `InteractionTracker`,
763
- compositor-driven touch and touchpad scrolling.
764
-
765
- Or the visual layer from the start: its drawing surface takes the same
766
- `BeginDraw`, `Scroll` and `Resize` (`ICompositionDrawingSurfaceInterop`),
767
- so the window's content and its promoted nodes could share one tree —
768
- which would also sidestep the open question of whether a classic target
769
- and a visual-layer target can share one window. The choice is deferred to
770
- Phase 4 on purpose and settled by a probe rather than by taste, because
771
- the visual layer's costs are the kind only a measurement prices: Windows
772
- 10 1803 as a floor; a `DispatcherQueue` on the thread that creates its
773
- compositor; and no explicit `Commit`, where classic DirectComposition
774
- commits exactly when told — the atomic frame the node model relies on.
775
-
776
- Whichever holds it, Windows adds one rule to the promotion policy that
777
- macOS did not need: a promoted node's surface is premultiplied unless its
778
- box is opaque, and Direct2D draws text in grayscale rather than ClearType
779
- on a premultiplied surface, so a node with text would visibly change its
780
- text rendering as it promoted and demoted. A node promotes on Windows if it is
781
- opaque or has no text. The rest of the policy — the z-order rule, the
782
- one-second grace — is `promotion.js`'s and transfers either way.
783
-
784
- ### Swapchains where they belong
785
-
786
- - **`<glarea>`**: GL on Windows means ANGLE — EGL and GLES translated to
787
- Direct3D 11, what Chromium, Firefox and Qt ship — presenting into a
788
- composition swapchain on a child visual, the way the Cocoa `<glarea>` is
789
- an IOSurface sublayer; its waitable object paces that visual. Desktop
790
- OpenGL through the vendors' drivers exists too, but reaching a
791
- composition visual from it needs `WGL_NV_DX_interop2`, a driver's promise
792
- rather than the system's. ANGLE is two DLLs to ship, optional the way
793
- `x11-dri` is. **What shipped is the other way round**
794
- ([windows-gl.md](windows-gl.md)): a WGL context on the vendor driver,
795
- through `WGL_NV_DX_interop2` into a composition swapchain on its own
796
- visual, with ANGLE the unbuilt rung below it for a machine without one.
797
- - **Element-owned surfaces**
798
- ([architecture/element-layer-contents.md](architecture/element-layer-contents.md)):
799
- a visual with a surface of its own, so a terminal's or a chart's frame is
800
- its own `BeginDraw` and the window's `Commit`, with the window's surface
801
- untouched — the constant-cost flip that design wants.
802
- - **Video**: Media Foundation decodes into NV12 textures, and a YUV
803
- swapchain on its own visual lets the display's overlay planes scan it
804
- out without a composition pass, where the GPU has them. Its own design:
805
- [architecture/video.md](architecture/video.md) §4 is the plan for this
806
- backend — `IMFMediaEngine` in frame-server mode first, drawn in paint
807
- order, the windowless swapchain onto a visual once promotion can decide
808
- what is drawn over it — and §4.3 the probes it owes before any of it is
809
- written.
810
- - The **composition swapchain** API of Windows 11 (present at a target
811
- time, per-buffer availability, statistics that include the displayed
812
- time) belongs to these three, not to the 2D surface.
63
+ - **Every window is composited by DWM**, so `transparent` always works and
64
+ `useSupports('transparency')` is always true — there is no "if a compositor
65
+ is running".
66
+ - **ClearType on opaque windows, grayscale on transparent ones.** Direct2D
67
+ draws subpixel text only onto a surface whose alpha is ignored, so text in
68
+ a `<window transparent>` or `<popup transparent>` is grayscale-antialiased.
69
+ - **Frames are paced by DWM's compositor clock**, at the refresh rate of the
70
+ monitor under the window; `frameRate` works as on the other backends. An
71
+ idle app polls nothing.
72
+ - Only damaged rectangles are repainted, and scrolling is a blit, as on X11
73
+ and Cocoa.
74
+ - **Reading pixels back**: `window.snapshot()` works once the window has
75
+ been composed. It rejects for a minimised or not-yet-composed window; draw
76
+ into an offscreen surface and read that instead.
77
+ - `<video frames>` draws; `<video src>` does not —
78
+ `useSupports('mediaPlayback')` is false here.
79
+ - Debugging: `REACT_X11_WIN32_DEBUG=1` reports every frame and what it drew
80
+ into, the size each paragraph is shaped at, and drag and drop;
81
+ `REACT_X11_WIN32_FULL_REPAINT=1` paints the whole window every frame, the
82
+ control for a damage bug.
83
+
84
+ ## Scale and DPI
85
+
86
+ - Windows are **per-monitor-v2 DPI aware** whatever `node.exe`'s manifest
87
+ says, so nothing is bitmap-stretched and layout runs in device pixels.
88
+ - **Fractional scale is the normal case** (125%, 150%, 175%). Layout rounds
89
+ to whole device pixels, so a one-logical-pixel hairline at 1.25× lands on
90
+ one device pixel or two depending on where it falls
91
+ ([scale.md](scale.md)).
92
+ - `useScale()` is the **primary monitor's** scale when the app starts, static
93
+ for the life of the root as on every backend, and every window is laid out
94
+ at it.
95
+ - `useScreens()` lists every monitor; its `available` is the monitor's exact
96
+ work area.
97
+ - **Not yet: live per-window scale.** A window on, or dragged to, a monitor
98
+ at another scale is not re-laid out at that scale: it stays sharp but is
99
+ the wrong physical size there.
813
100
 
814
101
  ## Text: DirectWrite behind the engine contract
815
102
 
816
- The contract is the one `src/cocoa/fonts.js` states at its top, and the
817
- Windows engine answers it over DirectWrite — the decision macos.md made for
818
- CoreText, for the same reasons: the platform's text system is what makes
819
- text look like the platform's, and fontconfig-shaped font resolution is
820
- what issue #86 documents going wrong off Linux. DirectWrite is used on the
821
- JS thread only, one layout at a time, which sidesteps the question
822
- Microsoft's documentation leaves open — whether a shared factory is safe to
823
- use from several threads at once.
824
-
825
- | contract | DirectWrite |
826
- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
827
- | `fonts.layout(spans, base, { maxWidth, align, lineHeight, maxLines, overflow, direction })` | an `IDWriteTextLayout` over the joined string, the spans as formatting ranges — family, size, weight, style, and a drawing effect for colour; width, alignment and reading direction as its properties; `lineHeight`, a multiplier over the natural line height, as proportional line spacing |
828
- | `width`, `height`, `lines` | `GetMetrics`, `GetLineMetrics` — in DIPs, answered in whole pixels per the yoga content-floor rule |
829
- | `indexAt`, `caretPosition`, `rangeBands` | `HitTestPoint`, `HitTestTextPosition`, `HitTestTextRange`. DirectWrite is UTF-16 like CoreText, so the code-point conversion stays at the same boundary |
830
- | `draw(ctx, x, y)` | `DrawTextLayout` on the device context, colour fonts enabled |
831
- | `coverage({ pad })` | the layout drawn through a text renderer that draws nothing: each glyph run through `IDWriteGlyphRunAnalysis` — grayscale, grid fit off, positions unsnapped — composited into a byte a pixel, with none of the gamma or enhanced contrast Direct2D gives text it blends (`layoutCoverage`, #673). Null on a bridge without it |
832
- | the min-content floor — a width offer of zero | `DetermineMinWidth` answers it natively, where CoreText cannot, which is why the Cocoa engine breaks at the `linebreak` package's opportunities itself. Keeping the `linebreak` rule on both native backends makes them agree on where a line may break; whether DirectWrite's own breaker already agrees is a measurement |
833
- | eliding, `maxLines` | trimming with an ellipsis sign for one line; for several, the cap and the cut are made in the text, as `cappedToEllipsis` does on Cocoa |
834
- | `fonts.match`, the generic families | the system font collection: `sans-serif` and `system-ui` → Segoe UI (Segoe UI Variable on Windows 11, which carries an optical-size axis the way SF does); `monospace` → Consolas, or Cascadia Mono where installed |
835
- | `fonts.fallbackFor(cp)` | the system font fallback (`IDWriteFontFallback::MapCharacters`) |
836
- | the face — `metrics`, `hasGlyph`, `glyphIdFor`, `advanceOf`, `shape` — and `ctx.drawGlyphs` | `IDWriteFontFace`'s glyph indices and design metrics, `IDWriteTextAnalyzer` for `shape`, `DrawGlyphRun`: the #432 run contract the terminal component draws with |
837
- | `openFont`, `loadFont` | an in-memory font file loader (Windows 10 1703) and a font set of the app's own; variable axes through `IDWriteFontFace5`; the WOFF unwrapping in `fonts.js` carries |
838
-
839
- Rendering quality is the one place Windows differs from macOS in kind:
840
- Windows users still see ClearType — subpixel antialiasing — through most of
841
- the system, where macOS dropped it. Direct2D draws ClearType only onto a
842
- target whose alpha is ignored, and silently falls back to grayscale on any
843
- other, which is why an ordinary window's surface is created that way, why
844
- `<popup transparent>` accepts grayscale, and why promotion has the rule
845
- above. Colour glyphs — emoji — draw through the COLR tables DirectWrite
846
- has read since Windows 8.1; COLRv1 is still a preview API.
847
-
848
- The recorded alternative is macOS's again: ntk's fontkit shaping and the
849
- Wayland plan's `'2d-sw'` rasterizer, for byte-identical metrics across
850
- backends at the cost of platform-correct text. The seam keeps it
851
- swappable; the default is DirectWrite.
103
+ Text is DirectWrite end to end — font matching, fallback, shaping, bidi, line
104
+ breaking, hit testing — so it looks like the platform's text, the way the
105
+ Cocoa backend uses CoreText.
106
+
107
+ - **Generic families**: `sans-serif`, `system-ui` and `ui-sans-serif` →
108
+ Segoe UI; `serif` → Times New Roman; `monospace` and `ui-monospace` →
109
+ Consolas; `cursive` → Segoe Script.
110
+ - **Font stacks** resolve to the first family installed. A face filed under
111
+ another family ("Arial Black", "Segoe UI Light") is found as GDI finds it,
112
+ and Courier/Courier New, Times/Times New Roman and Arial/Helvetica stand in
113
+ for each other.
114
+ - In laid-out text, characters the font lacks fall back to a system font,
115
+ as DirectWrite layouts do. `app.fonts.fallbackFor()` answers `null` (not
116
+ bound yet), so code drawing its own glyph runs gets no fallback face.
117
+ - `loadFont` and `openFont` load the app's own fonts, variable axes kept;
118
+ `style.variations` sets axes.
119
+ - Measurements are rounded up to whole device pixels, and lines break at the
120
+ same opportunities as on Cocoa, but metrics are DirectWrite's: expect a
121
+ pixel or so of difference from the X11 backend's layouts.
852
122
 
853
123
  ## Input
854
124
 
855
125
  ### Pointer
856
126
 
857
- - The mouse stays on the classic messages: `WM_MOUSEMOVE`, the button
858
- messages, `WM_MOUSELEAVE` from `TrackMouseEvent` (the X11 leave), and
859
- `SetCapture` while a button is held, which keeps a drag's motion coming
860
- after the pointer leaves the window. Click counting stays the renderer's
861
- (`detail`), with the double-click interval and distance from
862
- `GetDoubleClickTime` and `SM_CXDOUBLECLK` where X11 reads XSETTINGS.
863
- - The wheel's unit is `WHEEL_DELTA`, 120 to a notch. A precision touchpad
864
- sends high-precision wheel messages by default — deltas as small as 1 —
865
- and the only opt-out is a setting in the executable's manifest, so an
866
- addon always gets them; the wheel pipeline already takes fractions of a
867
- notch as `smooth` scrolling. `SPI_GETWHEELSCROLLLINES` says what a notch
868
- is worth. Whether Windows adds inertia to those messages on its own is a
869
- probe.
870
- - Pen and touch arrive as `WM_POINTER` messages whether or not the mouse
871
- does; Chromium takes exactly those two there and the mouse on the classic
872
- messages. Making the mouse a pointer too is `EnableMouseInPointer`, a
873
- switch for the whole process that can be thrown once and never undone,
874
- which an addon inside `node.exe` has no business throwing. The mouse
875
- messages Windows synthesizes from pen and touch carry a signature
876
- (`GetMessageExtraInfo`) that is per thread — read in the window procedure
877
- on the UI thread and forwarded with the event — and are dropped where the
878
- pointer message was already handled.
879
- - Touchpad inertia, pinch and rubber-banding the way Windows' own apps have
880
- them is Direct Manipulation: on `DM_POINTERHITTEST` for a touchpad
881
- contact, the UI thread hands the contact to a viewport, and Direct
882
- Manipulation recognizes the pan and the inertia — what Chromium, Firefox
883
- and Flutter do, on their UI threads. On the visual layer
884
- `InteractionTracker` is the same thing inside the compositor. Phase 5:
885
- the wheel path scrolls correctly without it.
127
+ - All five mouse buttons and the wheel. A drag that leaves the window keeps
128
+ receiving motion while a button is held.
129
+ - A precision touchpad sends fractions of a notch; those arrive as wheel
130
+ events with `smooth: true`.
131
+ - Motion and wheel are coalesced to one per frame, as on X11.
132
+ - Not yet: pen- and touch-specific events, touchpad inertia, pinch.
886
133
 
887
134
  ### Keyboard
888
135
 
889
- - The UI thread runs `TranslateMessage`, which queues the `WM_CHAR` a key
890
- typed behind its `WM_KEYDOWN`. The addon pairs them — the down, then the
891
- characters it produced, peeked off the queue — so JS sees one event with
892
- `keysym`, `codepoint`, `key` and `repeat`, the shape `events.js` reads on
893
- every backend. winit does the same, with a queue of pending events,
894
- because peeking can re-enter the window procedure through sent messages.
895
- Characters outside the Basic Multilingual Plane arrive as two `WM_CHAR`s,
896
- joined before they cross.
897
- - `keysym` comes from the virtual-key code through a table like
898
- `keymap.js`'s kVK one for the editing, navigation and function keys, and
899
- from the typed character otherwise, by the X rule the Cocoa keymap
900
- already implements: Latin-1 as itself, the rest at `0x01000000` plus the
901
- code point.
902
- - **Shortcuts under other layouts** come from the virtual key, and the
903
- layouts already assign those the way `accelerators: 'latin'` wants: the
904
- Russian layout gives each letter key the VK of the QWERTY letter in the
905
- same position, and AZERTY gives each key the VK of the letter printed on
906
- it. Ctrl+C is where the user expects it under both, with no code of ours.
907
- - **AltGr** arrives as Right-Alt with a synthesized Left-Control in front of
908
- it. The addon drops a Left-Control whose next queued message is the
909
- Right-Alt with the same timestamp, on layouts that have AltGr — the rule
910
- winit, Qt, GLFW and SDL share — and reports the level shift it is, so `@`
911
- on a German layout types and never matches a Ctrl+Alt chord. The known
912
- issue the README lists for X11 is absent here, as on Cocoa.
913
- - Dead keys compose in the layout itself (`WM_DEADCHAR`, then the composed
914
- `WM_CHAR`), so this backend defaults `compose` to the system's, as the
915
- Cocoa one gets the character with the event — and nothing calls
916
- `ToUnicodeEx` without the flag that keeps it from consuming the dead-key
917
- state.
918
- - **The keyboard can have a press state.** A held key repeats as
919
- `WM_KEYDOWN` with the previous-state bit set and no key-up in between,
920
- where X11 sends release/press pairs for auto-repeat — which is why
921
- AGENTS.md lists "the keyboard has no press state" as a known gap. On this
922
- backend `:active` for a held Space or Enter is drawable.
923
- - `useKeyboardState()`: `GetKeyState` for the locks before the first key,
924
- `WM_INPUTLANGCHANGE` for the layout.
136
+ - Key events carry the same X keysym vocabulary, `codepoint` and `key` as on
137
+ every backend; the editing, navigation, function and keypad keys have their
138
+ X keysyms.
139
+ - Dead keys compose in the layout, as the layout defines them; the backend
140
+ never consumes the dead-key state behind your back.
141
+ - Tab and Return never insert a control character into a field.
925
142
 
926
143
  ### IME
927
144
 
928
- **Built, on IMM32** — `windows/src/ime.cc` and `src/win32/ime.js`. Input
929
- methods are where this backend is ahead of X11 and Cocoa: X11 has none
930
- ([#272](https://github.com/sidorares/react-x11/issues/272)) and Cocoa's
931
- `NSTextInputClient` is unbuilt; Wayland's text-input
932
- (`src/wayland/textinput.js`) is the one other backend that composes. The
933
- surface they arrive on existed already — the composition state machine in
934
- `events.js` (`CompositionStart`, `Update`, `End`, each defaultable) and the
935
- preedit model in `src/nodes/preedit.js`, which keeps a composition out of the
936
- value and the undo history — so what was written is a transport and nothing
937
- else.
938
-
939
- What it does:
940
-
941
- - The composition **string** is the application's and the composition
942
- **window** is suppressed (`WM_IME_SETCONTEXT` minus
943
- `ISC_SHOWUICOMPOSITIONWINDOW`), because the preedit belongs in the field,
944
- where it is laid out and styled with the rest of the text. The **candidate
945
- list** is not suppressed: choosing among homophones is the input method's
946
- own interface and its vocabulary, not this application's.
947
- - The order is the one `src/wayland/textinput.js` `_apply` fixes — **the
948
- commit first, then the new preedit** — because that is the order the
949
- renderer's events are defined in rather than a detail of either protocol.
950
- One `WM_IME_COMPOSITION` can carry both.
951
- - `GCS_COMPATTR`'s target clause becomes the preedit cursor
952
- (`cursorBegin`/`cursorEnd`), which is the same fact Wayland's text-input
953
- carries under that name, so the field's existing preedit styling draws it.
954
- - Offsets cross the bridge as UTF-16 code units and are converted to code
955
- points in JS, where the string is.
956
- - `ImmSetCompositionWindow` and `ImmSetCandidateWindow` are fed from the
957
- caret rect, pushed after each frame — the layout the rect is read off is
958
- the one just painted.
959
- - A `sensitive` field is not offered to an input method at all: its context
960
- is disassociated, so the text never reaches a word history. Windows does
961
- the same for its own password boxes.
962
- - `VK_PROCESSKEY` is forwarded and **not** decoded, or every keystroke of a
963
- composition would also type its Latin letter into the field; `WM_IME_CHAR`
964
- is swallowed, since the committed text has already arrived as
965
- `GCS_RESULTSTR`.
966
-
967
- Two Windows APIs can feed this, and the second is still ahead:
968
-
969
- | toolkit | IME API |
970
- | -------------------------- | --------------------------------------------------------------- |
971
- | Chromium | TSF (`ui/base/ime/win/tsf_text_store.cc`) |
972
- | Firefox | TSF, with an IMM32 fallback (`widget/windows/TSFTextStore.cpp`) |
973
- | Qt 6, winit, Flutter, SDL3 | IMM32 |
974
- | GLFW | no composition at all |
975
-
976
- - **IMM32** (built) — `WM_IME_STARTCOMPOSITION`; `WM_IME_COMPOSITION`, with
977
- `ImmGetCompositionString` for the preedit, its cursor, its clause
978
- attributes and the committed result; `WM_IME_ENDCOMPOSITION`; and
979
- `ImmSetCompositionWindow`/`ImmSetCandidateWindow` fed from the caret rect
980
- JS pushed. Every message carries its payload, so the UI thread forwards
981
- and nothing waits. Reconversion works through it (Qt handles it). What
982
- it misses: Windows has stopped loading IMM32 input methods, so every
983
- modern one — the new Microsoft Japanese and Chinese IMEs included — is a
984
- TSF text service reaching an IMM32 window through a compatibility layer,
985
- and what that layer does not carry is voice typing (Win+H inserts
986
- nothing), the shell's handwriting, and shape-writing on the touch
987
- keyboard.
988
- - **TSF** — the application implements a text store (`ITextStoreACP2`) that
989
- the input method queries synchronously, on the thread that activated TSF:
990
- the text around the caret, the selection, the screen rectangle of any
991
- range. A synchronous lock request must be granted inside the call or
992
- refused, and a rectangle may be answered "no layout yet" — so the store
993
- never waits on JS. It answers from a mirror of the focused field — a
994
- window of its text, the selection, the caret geometry — pushed per commit
995
- and versioned, with edits flowing back as events and change notifications
996
- flowing out when JS rewrites the field under an open composition.
997
-
998
- **IMM32 first, TSF as the target.** IMM32 fits the threading rules with no
999
- mirror and carries CJK composition and candidate lists, and is what is built;
1000
- TSF is Phase 5, with a mirror scoped to the focused field and nothing else.
1001
- The compatibility-layer gaps above — voice typing, handwriting,
1002
- shape-writing — are the price of the first rung and the reason there is a
1003
- second one. The touch
1004
- keyboard, separately, appears for a focused text field because UI
1005
- Automation says it is one — not because of either API (§Accessibility).
145
+ Input methods work, through IMM32 — Chinese, Japanese and Korean composition
146
+ reach the same `onCompositionStart`/`Update`/`End` events a Wayland
147
+ text-input drives, so one handler serves both.
148
+
149
+ - The **preedit is drawn in the field**, laid out and styled with the rest of
150
+ the text; Windows' floating composition window is suppressed. The
151
+ **candidate list** is the input method's own and follows the caret.
152
+ - A commit arrives before the next preedit, and the active clause is the
153
+ preedit cursor (`cursorBegin`/`cursorEnd`), as on Wayland.
154
+ - A `sensitive` field is not offered to the input method at all, so its text
155
+ never reaches a word history — as Windows does for its own password boxes.
156
+ - **What IMM32 does not carry**: modern input methods are TSF text services
157
+ reaching IMM32 windows through a compatibility layer, and that layer drops
158
+ **voice typing** (Win+H inserts nothing), the shell's **handwriting**
159
+ panel and **shape-writing** on the touch keyboard. A TSF text store, which
160
+ would close those, is not built. Reconversion is not wired up either.
161
+ - `REACT_X11_TRACE_IME=1` prints a line per input-method event.
1006
162
 
1007
163
  ## Windowing semantics: what maps, what bends, what breaks
1008
164
 
1009
- | react-x11 today | Windows | verdict |
1010
- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
1011
- | `<window>`, WM-managed | a top-level HWND; geometry, activation and close requests arrive as messages on the UI thread | maps; the app places itself, unlike on Wayland |
1012
- | `<window x y>` | `SetWindowPos` in virtual-screen coordinates, which go negative left of and above the primary monitor | maps |
1013
- | auto-sizing | measure, then size before the first show — the `realize()` order | maps |
1014
- | `<popup>` | an owned `WS_POPUP` with `WS_EX_NOACTIVATE` and `WS_EX_TOOLWINDOW` (no taskbar button, no Alt+Tab entry), placed by `anchor.js` against the monitor's work area; rounded and shadowed on Windows 11 by asking DWM, which treats the corner preference as a hint | maps — client-side placement survives, as on macOS |
1015
- | `<popup grab>` dismissal | **built**: no cross-application pointer grab exists (`SetCapture` holds only while a button is down), so dismissal watches presses in the app's other windows and the loss of activation. A press on our own non-client area is the one case it misses | bends — the same behaviour by another mechanism |
1016
- | `decorations: false`, a drawn titlebar | `WM_NCCALCSIZE` returns 0, `DwmExtendFrameIntoClientArea` keeps the shadow and snapping, `WM_NCHITTEST` answers from pushed regions — `HTMAXBUTTON` for Snap Layouts — and caption buttons route through `DwmDefWindowProc` | maps, given a region API (open question 7) |
1017
- | maximized, minimized, fullscreen, attention | `ShowWindow`; a borderless monitor-sized window; `FlashWindowEx` | maps |
1018
- | `alwaysOnTop`, `skipTaskbar`, `below` | `HWND_TOPMOST`; `WS_EX_TOOLWINDOW` or `ITaskbarList::DeleteTab`; `HWND_BOTTOM`, which is not kept | maps — more of `_NET_WM_STATE` than macOS keeps; `sticky` and `shaded` have no public equivalent |
1019
- | `transientFor` | the owner window, set at creation | maps — an owned window stays above its owner and minimizes with it |
1020
- | `appId` | **built**: the window's AppUserModelID, set on its property store before it is shown, and the id the jump list is attached to. The relaunch properties (`PKEY_AppUserModel_Relaunch*`), which decide what a pinned tile starts, are still open | maps — the identity exists, under another name |
1021
- | `transparent` | `WS_EX_NOREDIRECTIONBITMAP` and premultiplied DirectComposition content | maps better — always composited; `useSupports('transparency')` is constant-true |
1022
- | frame clock (Present, fence, estimator) | the compositor clock or the vblank, and on Windows 11 the compositor's statistics | simpler, and on Windows 11 photon time is measurable |
1023
- | the scale ladder | per-monitor-v2 on the UI thread; `WM_DPICHANGED` with the suggested rect taken as given | collapses to one live answer, fractional most of the time — and live means re-scaling (§Layout) |
1024
- | `useScreens()` | `EnumDisplayMonitors`; `GetMonitorInfo`, whose work area is `available` exactly where X11's is an approximation; names and refresh rates from the display-configuration API; `WM_DISPLAYCHANGE` — queried on the UI thread, which is DPI-aware, and published | maps |
1025
- | clipboard | `CF_UNICODETEXT`, `CF_HDROP`, `HTML Format`, PNG; delayed rendering for lazy payloads, answered on the owner window's thread; `AddClipboardFormatListener` for `watch()`; no `PRIMARY` | maps; `transfer.js`'s MIME plumbing is reusable |
1026
- | drag and drop | OLE — `RegisterDragDrop` and `IDropTarget`, `DoDragDrop` and `IDropSource`, shell drag images; virtual files for a drag out that makes its file on demand. Windows' integrity levels refuse a drop from Explorer into an elevated app | maps — the `dropAccept`/`onDrag*`/`dragData` contract holds, and JS keeps running through the drag |
1027
- | global menu | none: the drawn `MenuBar`, the fallback path anyway; a native `HMENU` bar as an opt-in rung; `ContextMenu` over `TrackPopupMenuEx` as its native rung, as `NSMenu` is on Cocoa | bends |
1028
- | `<foreign>` | a cross-process child window is allowed, and costly in a known way: the two programs' input queues are attached, so a hung child hangs the parent; no XEmbed focus protocol | bends |
1029
- | `<Frame>` | the **pane** makes a composition surface handle, presents into it through a swapchain made for it, and duplicates it into the host, whose visual tree shows what the pane draws — the IOSurface design, with the handle named once instead of per frame ([windows-embedding.md](windows-embedding.md)) | maps |
1030
- | `examples/wm.jsx`, framing other programs' windows | — | **gone by design** |
1031
- | startup notification, `activateWindow` | Windows' own launch feedback; `SetForegroundWindow` under the foreground rules, where the second instance forwarding a URI first grants the running one the right to come forward (`AllowSetForegroundWindow`) | dissolves; the raise maps, with its own version of the X11 timestamp lesson |
1032
- | URI schemes, single instance | the scheme registered under `HKCU\Software\Classes` (no admin — the `.desktop` file's job), a named pipe or `WM_COPYDATA` to the running instance | maps — the two-copies shape `application.js` already handles |
1033
- | tray | `Shell_NotifyIcon`, its menu through `TrackPopupMenuEx` | **gained**; the freedesktop rung is StatusNotifierItem (#353) |
1034
- | `<glarea>` | **built**: a WGL context through `WGL_NV_DX_interop2` into a composition swapchain; ANGLE the unbuilt rung below ([windows-gl.md](windows-gl.md)) | bends — a driver's extension, not the system's |
1035
- | eyedropper | no system sampler: a rung of our own, reading the screen without a permission prompt; windows that exclude themselves from capture read as absent | maps — the X11 rung's shape |
1036
- | global key grabs | `RegisterHotKey`, system-wide, no admin — registered against a window on the UI thread, because a thread hotkey's message is eaten by modal loops; a combination another app holds fails, and the Windows-key combinations are reserved | maps — which macOS allows only behind a permission |
1037
- | `ssh -X` remoting | — | the X11 backend remains the remote answer |
1038
-
1039
- The `<popup>` row is macOS's conclusion, not Wayland's: Windows keeps
1040
- client-side placement, so `anchor.js` stays the single source of truth,
1041
- reading monitor work areas instead of Xinerama. The row that is new is the
1042
- titlebar: a drawn one works on every backend today by moving the window
1043
- from `onMouseMove`, and on Windows that loses snapping, the Snap Layouts
1044
- flyout and the system's own drag — which is why the region API is an open
1045
- question for all four backends rather than a Windows-only prop.
165
+ **Maps**:
166
+
167
+ - `title`, `width`, `height`, `x`, `y` and the natural size. `x`/`y` are
168
+ virtual-screen coordinates, negative left of and above the primary monitor.
169
+ - `minWidth`/`minHeight`/`maxWidth`/`maxHeight` and `resizable` become the
170
+ window's size limits.
171
+ - `fullscreen`, `alwaysOnTop`, maximise and minimise use Windows' own calls;
172
+ full screen is a borderless monitor-sized window that remembers the frame
173
+ it came from.
174
+ - `transparent` — always available ([Rendering](#rendering)).
175
+ - `appId` — the window's AppUserModelID
176
+ ([Packaging and identity](#packaging-and-identity)).
177
+ - `<popup>` — no taskbar button, never takes activation, placed against the
178
+ monitor's work area as everywhere.
179
+ - Drag and drop, both directions: `dropAccept`, `onDrag*` and `dragData`
180
+ hold, and JS keeps running through the drag. Windows itself refuses a drop
181
+ from Explorer into an elevated app.
182
+
183
+ **Bends**:
184
+
185
+ - `<popup grab>`: there is no pointer grab on Windows, so a press in another
186
+ of the app's windows, or the app losing activation, closes the popup
187
+ instead. The gap: a press on one of the app's own title bars or resize
188
+ borders does not, so a menu stays open while its window is dragged.
189
+ - `states`: `shaded` and `sticky` have no Windows call and resolve false,
190
+ which `useWindowState()` reports. `focused` can be refused by Windows'
191
+ foreground rules, and then simply does not change.
192
+ - `embeddable` and `<foreign>`: see
193
+ [Embedding](#embedding-frame-foreign-window-embeddable).
194
+ - The global menu: Windows has none; draw a `<MenuBar>`.
195
+
196
+ **Inert** — accepted and ignored, so shared code stays branch-free:
197
+
198
+ - `decorations={false}`, today. A titlebar drawn in the window and moved
199
+ from `onMouseMove` loses snapping and the Snap Layouts flyout.
200
+ - `transientFor`, `windowType`.
201
+ - `grabKeyboard`: a popup gets keys only while it is the foreground window.
202
+ - `onClientMessage` never fires; there is no X.
203
+
204
+ **Breaks**, by design: window managers (`examples/wm.jsx`) — DWM owns every
205
+ frame — and `ssh -X` remoting, for which the X11 backend stays the answer
206
+ ([remote.md](remote.md)).
1046
207
 
1047
208
  ## The desktop around the app
1048
209
 
1049
- Every ladder in the README gets a Windows rung or an honest floor. The
1050
- D-Bus and portal rungs are absent (§"What already carries over"); the
1051
- shell-out rungs have Windows equivalents only where they are worth having.
1052
-
1053
- This table is the design. For what is built,
1054
- [windows-integrations.md](windows-integrations.md) is the measured status —
1055
- every row of it driven on hardware rather than inferred from an API existing.
1056
-
1057
- | what | the Windows rung | below it |
1058
- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
1059
- | open/save panels, `useFileDialog()` | the Common Item Dialog (`IFileOpenDialog`, `IFileSaveDialog`), parented to the window, its modal loop on the UI thread | the dialog react-x11 draws |
1060
- | notifications, `notify()` | toasts, which Windows shows only for an AppUserModelID it knows: a Start-menu shortcut carrying it, or its registration in the registry; the Windows App SDK's notification manager registers an unpackaged app for you, and brings its runtime | a tray balloon, which Windows shows as a toast; then "unsupported", said |
1061
- | light/dark and accent, `useSystemAppearance()` | `UISettings` (the accent and foreground colours, text scale and animation settings, each with a change event, which arrives off the UI thread and is marshalled to it) and the apps-use-light-theme preference under `HKCU`; high contrast from `SPI_GETHIGHCONTRAST`; reduced motion from `SPI_GETCLIENTAREAANIMATION`; the window frame follows through `DWMWA_USE_IMMERSIVE_DARK_MODE` | the answer remembered on disk, as today |
1062
- | permissions, `usePermission()` | none to ask: the capability-status API is for packaged apps, and an unpackaged one sits under a single "desktop apps" switch per device. The capture API's access-denied is the answer, and `ms-settings:privacy-webcam` and its siblings are the Settings-pane rung | `'unknown'` until something is attempted |
1063
- | the user's calendar | the appointments API wants package identity | `supported: false` unless packaged |
1064
- | sampling a screen colour, `useEyedropper()` | our own: the screen read through GDI — no consent, no capture border — plus a drawn loupe | — |
1065
- | a badge on the icon, `setBadge()` | a taskbar overlay icon (`ITaskbarList3::SetOverlayIcon`) with the count drawn into it, once the taskbar button exists | — |
1066
- | the launcher menu, `useLauncherMenu()` | a jump list, whose tasks relaunch the app with arguments that arrive through the single-instance path | — |
1067
- | a tray icon, `useTray()` | `Shell_NotifyIcon` | — |
1068
- | progress on the icon, `useProgress()` | `ITaskbarList3::SetProgressValue` | — |
210
+ No D-Bus or portals here: each service has a Windows rung or an honest
211
+ floor. [windows-integrations.md](windows-integrations.md) has the measured
212
+ status of each; in short:
213
+
214
+ - **Work**: `useFileDialog()` (Windows' own dialog, owned by the window that
215
+ asked), `useSystemAppearance()` (light/dark, accent, high contrast, reduced
216
+ motion; the window frame follows dark mode), `useTray()`, `useProgress()`,
217
+ `useBadge()`/`setBadge()` (drawn into the taskbar's overlay icon — pass one
218
+ character for a dot), `setUrgent()`, `useIdle()`, `keepAwake()`,
219
+ `useLocale()`.
220
+ - **Global shortcuts** (`useAccelerator()`): system-wide, no admin. A chord
221
+ another app holds fails; Windows-key chords are reserved.
222
+ - **Notifications** (`notify()`, `useNotifier()`) are tray balloons, which
223
+ the shell shows as toasts and keeps in the Action Center: plain text with a
224
+ severity — no actions, click events, updates or custom icon, as the
225
+ capabilities say.
226
+ - **Clipboard** (`useClipboard()`): text only — writing anything else
227
+ rejects. No `PRIMARY`. `watch()` notices a change within about 400 ms.
228
+ - **Windows only**: `useJumpList()`, `useThumbnailToolbar()`,
229
+ `useRecentDocument()`, reported as features of
230
+ `useDesktopCapability('launcher')`.
231
+ - **Answer "no"**: `useLauncherMenu()` is inert (a jump-list task starts a new
232
+ process — use `useJumpList()`); `useAppOpen()` and `useAppActivate()` never
233
+ fire (no file associations, URL schemes or single instance yet);
234
+ `useEyedropper()` reports `supported: false`; `usePermission()` answers
235
+ `'unknown'`; `useDesktopCalendarEvents()` is `supported: false` unless the
236
+ app is packaged.
237
+ - **Native control bezels** are the system's own while the desktop is light,
238
+ and drawn in dark mode, because Windows' dark common controls are not
239
+ public.
1069
240
 
1070
241
  ## Accessibility: UI Automation
1071
242
 
1072
- [architecture/accessibility.md](architecture/accessibility.md) records the
1073
- AT-SPI bridge's central decision: **no mirror tree**. Every call is
1074
- answered from the live node tree when it arrives, which works because
1075
- AT-SPI's calls arrive on the bus, asynchronously, and JS answers them when
1076
- it gets to them. UI Automation changes the premise that decision rested
1077
- on.
1078
-
1079
- A UIA provider is a set of COM objects — element, fragment and
1080
- fragment-root providers, and one per control pattern (Invoke, Toggle,
1081
- Value, RangeValue, ExpandCollapse, Selection, Scroll, Text) — handed to UIA
1082
- from `WM_GETOBJECT`, and UIA calls their methods synchronously and often:
1083
- one focus change is dozens of property reads. Where the calls arrive
1084
- depends on a flag. With `ProviderOptions_UseComThreading`, which Chromium
1085
- sets, they arrive on the provider's own apartment — the UI thread; without
1086
- it, on UIA's threads. So a provider that answered from the live tree would
1087
- block a thread on JS for every read: the UI thread, freezing input while a
1088
- screen reader reads, or a UIA thread, waiting on every read for JS to be
1089
- idle — through a React commit, a layout, a paint. It is legal — a client
1090
- waits 20 seconds by default — but it paces the screen reader by the app's
1091
- busiest moment.
1092
-
1093
- **Built: the mirror, with a provider of this project's own.**
1094
- `windows/src/uia.cc` serves UIA from a tree `src/win32/a11y.js` pushes —
1095
- roles, names, descriptions, states, bounding rectangles, focus, hit testing,
1096
- and the Invoke, Toggle, Value and RangeValue patterns, with `announce()` as a
1097
- UIA notification. `ProviderOptions_ServerSideProvider` without
1098
- `UseComThreading`, so every call arrives on a UIA thread, reads the mirror
1099
- under one lock and returns, and nothing ever waits on JS. A provider holds a
1100
- window id and a node id and nothing else, so a client keeping an element
1101
- across a re-render cannot make it read freed memory. `ITextProvider` and its
1102
- ranges are the piece that is not there; a `<textinput>` exposes its value
1103
- through `IValueProvider` instead. Commits push the mirror at the pace
1104
- [accessibility.md](accessibility.md#how-often-the-bridge-speaks) describes
1105
- for both bridges: at once after a quiet spell, once per interval through a
1106
- stream, and at once for focus, announcements and the focused element's own
1107
- changes.
1108
-
1109
- The alternative considered for the same shape was AccessKit, and the reason
1110
- it was not taken is the build rather than the design: its C bindings put Rust
1111
- in the release pipeline, for x64 and arm64 prebuilds both. The paragraph
1112
- below is the argument that chose the mirror, and it stands either way.
1113
-
1114
- **So the lean on this backend is the mirror, deliberately, and AccessKit
1115
- would have been the other way to get one.** Its Windows adapter serves UIA from a tree the app
1116
- pushes and answers from its own copy on UIA's threads, with text ranges for
1117
- single- and multi-line inputs (rich text not yet), and it links into a C++
1118
- addon through its C bindings. The push is what the AT-SPI bridge already
1119
- computes for its events: `a11y.js`'s model is pure functions over the live
1120
- tree, and the bridge's per-node snapshot of what the bus was last told is
1121
- the diff an AccessKit update is made of. Actions — invoke, set a value,
1122
- focus, scroll into view — come back as requests JS handles asynchronously.
1123
- It is Chromium's shape too: the browser answers UIA from its own copy of
1124
- the page's tree. And AccessKit's macOS adapter would fill the Cocoa
1125
- backend's missing `NSAccessibility` bridge from the same push. The cost is
1126
- the one architecture/accessibility.md named — a second copy of the truth,
1127
- kept honest here by the snapshot diff rather than by construction — and it
1128
- is paid because UIA's synchronous calls make it the cheaper side of the
1129
- trade, not because the AT-SPI decision was wrong.
1130
-
1131
- The alternative on record is a provider of our own answering from the live
1132
- tree through a bounded wait — no mirror, parity with AT-SPI — whose risk is
1133
- exactly the latency above. Phase 5 measures the two against Narrator and
1134
- NVDA before the choice is final; whether JAWS and NVDA read a UIA-only
1135
- tree as well as they read an IAccessible one is part of that measurement.
1136
- Either way UIA does a second job here: the touch keyboard appears for a
1137
- focused text field because UIA says it is one.
1138
-
1139
- ## Testing
1140
-
1141
- - **Any OS, in CI**: the backend's JS half against a fake bridge, the way
1142
- the Cocoa backend's 52 `test/cocoa-*.test.js` files run on Linux today —
1143
- including the pixel-holding fake `test/cocoa-scroll-blit.test.js` uses to
1144
- prove a blit against a repaint, which a DirectComposition `Scroll` needs
1145
- just as much.
1146
- - **On a Windows runner**: GitHub's hosted runners have an interactive
1147
- desktop in practice (at 1024×768), `windows-latest` is Windows Server
1148
- 2025, and ARM64 runners are generally available for public repositories.
1149
- Real windows there, over WARP when there is no GPU; the composited window
1150
- read through `PrintWindow` with `PW_RENDERFULLCONTENT` — undocumented on
1151
- Microsoft Learn, relied on by WebRTC and Firefox for exactly this content,
1152
- and stable only once the commit has completed; input posted into the
1153
- window procedure through the bridge, the way `postMouseEvent` drives the
1154
- Cocoa pump, and `robotjs` where the test wants the real input stack.
1155
- - **Fractional scale first**: the interaction tests run at 1.25 and 1.5, not
1156
- only 1 and 2, and a window crossing between two scales is a test of its
1157
- own. Logical and device pixels part company on every event, and a test at
1158
- scale 1 cannot see which of the two a coordinate is in.
1159
- - `react-x11/test` keeps driving X11, as it does on macOS; a `'win32-mock'`
1160
- harness backend is macos.md's Public API item 6 again.
1161
- - **The bench twin**: `npm run bench:presenters`' scenarios and its
1162
- structural gate — full-window frames, frames per resize tick, frames for a
1163
- window nobody can see — ported, plus the number no other backend can
1164
- report: input-to-photon from the compositor's statistics, on Windows 11.
243
+ Screen readers read the app through UI Automation. What an app writes —
244
+ `role`, `aria-*`, `announce()` — is read by the same code the Linux AT-SPI
245
+ bridge reads, so the two cannot disagree about what a node is.
246
+
247
+ - **Crosses**: roles, names, descriptions, states, bounding rectangles,
248
+ focus and hit testing; the Invoke, Toggle, Value and RangeValue patterns;
249
+ `announce()` as a UIA notification.
250
+ - The bridge answers from a copy of the tree the app pushes after commits, so
251
+ a screen reader never waits on a busy JS thread. Pushes are paced as
252
+ [accessibility.md](accessibility.md#how-often-the-bridge-speaks) describes.
253
+ - **Missing: text ranges.** A `<textinput>` exposes its value, so a screen
254
+ reader reads the field and hears edits, but cannot move caret by caret or
255
+ word by word inside it.
256
+ - `REACT_X11_TRACE_A11Y=1` prints a line per push and action.
257
+
258
+ ## 3D: `<glarea>`
259
+
260
+ `<glarea>` draws through OpenGL on the **GPU vendor's driver**, and its frames
261
+ are composited with the rest of the window, the window's alpha included.
262
+
263
+ - The context is an OpenGL ES-profile one: GLSL ES shaders, and the
264
+ WebGL 2-shaped `gl` (`gl.bindBuffer`, …) of the X11 direct backend.
265
+ `@react-x11/components/three` runs on it. No `glPolicy` is needed: this
266
+ backend is always direct.
267
+ - It needs the driver's `WGL_NV_DX_interop2`, which NVIDIA and AMD drivers
268
+ have and some Intel configurations lack.
269
+ - **Without a vendor driver** — a virtual machine without GPU passthrough, a
270
+ CI runner, a fresh install before Windows Update fetched the driver, some
271
+ Remote Desktop sessions — Windows offers only the OpenGL 1.1 software
272
+ rasterizer, which has no shaders. `<glarea>` then reports one `onError`
273
+ naming the reason, and `useSupports('shaders')` is false. The ANGLE
274
+ fallback that would cover these machines is not built.
275
+ - `useSupports('shaders')` is false for one tick and then true: the GL probe
276
+ (about 120 ms) runs the first time something asks, not at startup.
277
+ - A `<glarea>`'s children are drawn on composited panes above the surface,
278
+ translucency included (`useSupports('glOverlay')` is true). The surface
279
+ takes no pointer input; the pointer reaches the tree.
280
+
281
+ Background: [design/windows-gl.md](../design/windows-gl.md).
282
+
283
+ ## Embedding: `<Frame>`, `<foreign>`, `<window embeddable>`
284
+
285
+ The API is the same as everywhere; what answers it differs.
286
+
287
+ - **`<Frame src>` works.** The pane runs in its own process and draws into a
288
+ composition surface the host shows; `props`, `fallback` and `restart()`
289
+ behave as on every backend ([frame.md](frame.md)). The host is the pane's
290
+ parent process; a pane started some other way names it with
291
+ `createRoot({ win32: { paneHostPid } })`.
292
+ - **`<foreign>` refuses.** Windows' cross-process child windows attach the two
293
+ programs' input queues, so a hung guest would hang the host.
294
+ `useSupports('embedding')` is false, and a `<foreign>` reports one
295
+ `onError`:
296
+
297
+ ```
298
+ react-x11: <foreign> needs a backend with cross-process window embedding,
299
+ and this one has none — so nothing can be put in it. Ask
300
+ useSupports('embedding') before rendering one.
301
+ ```
302
+
303
+ ```jsx
304
+ const embedding = useSupports('embedding');
305
+ return embedding ? <foreign onReady={spawnInto} /> : <text>X11 only</text>;
306
+ ```
307
+
308
+ - **`<window embeddable>`** is created and not shown, as everywhere.
309
+ `windowHandleOf(ref)` / `useWindowHandle(ref)` returns a **composition
310
+ surface handle** already valid in the host process — not an HWND, because a
311
+ composition target stops presenting once its window is made a child. Pass
312
+ it to the host out of band, as an XID is passed on X11. The host must bind
313
+ a composition surface to a visual of its own (another react-x11 app, WinUI
314
+ 3, a DirectComposition-based host); a plain Win32 or OLE document host
315
+ cannot. The host is the parent process unless
316
+ `createRoot({ win32: { paneHostPid } })` names another.
317
+ - OLE embedding (a pane inside a Word document) is not supported.
318
+
319
+ Background: [design/windows-embedding.md](../design/windows-embedding.md).
1165
320
 
1166
321
  ## Packaging and identity
1167
322
 
1168
- - `node app.js` works, with two problems that are cosmetic only to a
1169
- developer. `node.exe` is a console program, so launched from a shortcut it
1170
- opens a console window. And the taskbar groups every `node.exe` window
1171
- together under Node's icon, and pinning pins `node.exe` — the Windows
1172
- version of the menu bar saying "node". The second is the addon's to fix:
1173
- each window gets the app's AppUserModelID and relaunch properties (from
1174
- the `appId` `registerApplication()` already takes) and the app's icon.
1175
- The first is a packaging step.
1176
- - A **single executable** ([packaging.md](packaging.md) tier 3;
1177
- `node --build-sea` since Node 25.5) is a copy of `node.exe` with the app
1178
- inside, and its resources can be rewritten after the build: a manifest of
1179
- the app's own with `rcedit`, the GUI subsystem with
1180
- `editbin /SUBSYSTEM:WINDOWS`, then the signature. At that point the
1181
- brief's manifest advice applies in full. Windows 11 24H2 adds a manifest setting that detaches the console
1182
- when launched from Explorer and keeps it in a terminal. The addon rides
1183
- beside the executable through `createRequire(process.execPath)`, as the
1184
- Cocoa one does, with `REACT_X11_WIN32_PATH` naming it for a compiled
1185
- bundle, as `REACT_X11_CALAYERS_PATH` does there.
1186
- - `bun build --compile` targets Windows with `--windows-hide-console` and
1187
- icon and metadata flags, and no way to supply a manifest — and `bun.exe`'s
1188
- own manifest has no compatibility section, which Microsoft documents as
1189
- running the process in Vista compatibility mode. What that changes for
1190
- DWM and DPI is a probe.
1191
- - Distribution: Authenticode signing, which SmartScreen's reputation
1192
- follows; and MSIX as an option — the package identity that unlocks the
1193
- calendar, the capability-status API and declared protocol activation.
1194
- - packaging.md grows a Windows tier when there is a Windows build to verify
1195
- it against.
1196
-
1197
- ## The plan
1198
-
1199
- Each phase has an exit that makes the next one safe to start.
1200
-
1201
- - **Phase 0 — the spike (days). Done, 2026-09-18.** A minimal addon proving
1202
- the risky mechanisms: the UI thread inside stock `node.exe` — and
1203
- `bun.exe`, which loads the addon and runs the event path — with events
1204
- through a threadsafe function; a DirectComposition surface driven from the
1205
- JS thread, `BeginDraw` per rect and `Commit`. _Measured: a JS timer kept
1206
- its cadence, 46 ticks against ~48 due over twelve seconds._ **Not yet
1207
- probed**: the bounded resize handshake, the background plate,
1208
- `WM_NCHITTEST` from pushed regions, and the cadence specifically through
1209
- `TrackPopupMenuEx` and `DoDragDrop`, which are what the claim rests on
1210
- hardest and which need a menu and a drag to exist first.
1211
- - **Phase 1 — the shared floor. Done.** `src/backend/context2d.js`, moved
1212
- out of `src/cocoa/` with the class renamed and Cocoa behaviourally
1213
- unchanged — 303 passing before and after. `surface.js`, the frame queue
1214
- and the promotion policy have **not** moved: the context was what a second
1215
- native backend actually needed, and moving the rest before anything
1216
- consumes it would be renaming for its own sake.
1217
- - **Phase 2 — the surface backend. Done.** `src/win32/` — app, window,
1218
- fonts, surfaces, bezels — over `@windowkit/win32`, with
1219
- `createRoot({ backend: 'win32' })` and `'auto'` choosing it on Windows.
1220
- 41 of the 43 examples open; the two that do not (`wm`, `xeyes`) are X11
1221
- window-manager demos that do not run on Cocoa either. The keyboard decodes
1222
- through `ToUnicodeEx` without consuming dead keys, all five mouse buttons
1223
- and the wheel route, and `<popup>` places menus against the monitor's work
1224
- area. **Outstanding against the original exit criteria**: the bench twin
1225
- with its baselines. The fake-bridge suite under `test/win32/` has landed
1226
- since, beside the bridge's own suite against the real thing.
1227
- - **Phase 3 — input, scale and the desktop. Mostly done.** Built and
1228
- verified on hardware: the clipboard, per-monitor-v2 scale and
1229
- `WM_DPICHANGED`, monitors, appearance (light/dark, accent, the frame
1230
- through `DWMWA_USE_IMMERSIVE_DARK_MODE`), window states, file dialogs, the
1231
- tray, the taskbar's progress, badge and flash, global hotkeys,
1232
- notifications as a balloon, idle time and keeping the screen awake.
1233
- Built since: IMM32 composition, drag and drop over OLE and the jump list
1234
- (`useJumpList`). **Not built**: single instance and URI schemes.
1235
- [windows-integrations.md](windows-integrations.md) is the row-by-row
1236
- status. _Exit: a window dragged between a 100% and a 150% monitor
1237
- re-lays out at each; every ladder in the README has a Windows rung or an
1238
- honest floor, each verified on hardware._
1239
- - **Phase 4 — the compositor tier.** Promotion onto composition visuals,
1240
- with the classic-or-visual-layer question settled by its probe;
1241
- element-owned surfaces. `<glarea>` is **done**, and not over ANGLE: a WGL
1242
- context on the vendor driver, with `WGL_NV_DX_interop2` lending its
1243
- texture to the composed surface, because a child HWND cannot composite
1244
- over a window presenting through DirectComposition. ANGLE stays the rung
1245
- below for a machine with no vendor driver, where the probe reports no core
1246
- context and `useSupports('shaders')` is false. _Exit:
1247
- `examples/animation.jsx`'s loops keep running through its deliberate JS
1248
- block, as they do on Cocoa._
1249
- - **Phase 5 — the deep integrations.** UI Automation; TSF; Direct
1250
- Manipulation; input-to-photon on the dashboard. _Exit: Narrator and NVDA
1251
- read the widgets example the way Orca reads it, and a Japanese IME
1252
- composes in place with its candidate list at the caret._
1253
-
1254
- ## What to probe first
1255
-
1256
- The checklist for the first session on a Windows machine, in the order the
1257
- design depends on the answers:
1258
-
1259
- 1. **The UI thread inside `node.exe`**: an HWND on a thread the addon
1260
- started, events through a threadsafe function, the command queue woken
1261
- through a message-only window; input-to-JS latency (a performance-counter
1262
- stamp at the message against one in the handler), idle and under a busy
1263
- JS loop; then the same under `bun.exe`, whose threadsafe functions have a
1264
- reported Windows panic.
1265
- 2. **Modal loops**: a JS `setInterval` keeps its cadence through a live
1266
- resize, a `TrackPopupMenuEx` menu, a `DoDragDrop` into Explorer and an
1267
- `IFileDialog::Show` — and the command queue keeps draining inside each.
1268
- 3. **DirectComposition across threads**: the device, and a target for the
1269
- UI thread's window, created on the JS thread, as the documentation
1270
- allows; `Commit` from JS while the UI thread is inside a modal loop; the
1271
- background plate's device and the content's device in one tree.
1272
- 4. **Surface costs**: what `Scroll` does exactly and what it costs —
1273
- neither browser calls it; what several `BeginDraw`s in one frame cost
1274
- against one covering their union; a virtual surface's resize under a
1275
- live drag.
1276
- 5. **Resize**: the bounded wait inside `WM_SIZE`, what DWM shows when the
1277
- deadline is missed, and whether returning needs the commit to have been
1278
- composed first — Flutter calls `DwmFlush` after the frame of the new size.
1279
- 6. **Scale**: `WM_DPICHANGED` and `WM_GETDPISCALEDSIZE` on a two-monitor
1280
- desk at 100% and 150%; the interim content-visual scale; what
1281
- `bun.exe`'s missing compatibility section changes.
1282
- 7. **Text**: ClearType on an alpha-ignored surface; `DetermineMinWidth`
1283
- against the `linebreak` floor; Segoe UI Variable's optical size.
1284
- 8. **Input**: whether Windows adds inertia to a precision touchpad's wheel
1285
- messages; the AltGr rule on a German layout; the pointer/mouse split on
1286
- a touch screen.
1287
- 9. **IME**: the Windows 11 Japanese and Chinese IMEs through IMM32 —
1288
- composition, clause attributes, the candidate window at a pushed caret.
1289
- 10. **UI Automation**: AccessKit's adapter behind our window procedure;
1290
- Narrator and NVDA on a two-button window; JAWS and NVDA on a UIA-only
1291
- tree.
1292
- 11. **Notifications**: a toast from an unpackaged `node.exe` with an
1293
- AppUserModelID registered through a shortcut, and through the registry
1294
- — whose hive the sources did not settle.
1295
- 12. **CI**: an HWND, a DirectComposition surface over WARP and a
1296
- `PrintWindow` capture on a GitHub-hosted Windows runner, x64 and ARM64.
323
+ - **`node app.js` works**, with two cosmetic problems. `node.exe` is a
324
+ console program, so launched from a shortcut it opens a console window.
325
+ And without an `appId`, the taskbar groups the app's windows with every
326
+ other `node.exe` window under Node's icon, and pinning pins `node.exe`.
327
+ - **`<window appId>`** (or the class half of `wmClass`) sets the window's
328
+ AppUserModelID, per window, before it is shown. The taskbar groups by it,
329
+ pinning pins it, and the jump list belongs to it. A pinned tile relaunches
330
+ the command line that started the process, named after the window's title,
331
+ with the executable's icon.
332
+ - **A single executable** ([packaging.md](packaging.md) tier 3,
333
+ `node --build-sea`) is a copy of `node.exe` with the app inside. After the
334
+ build: a manifest of the app's own with `rcedit`, the GUI subsystem with
335
+ `editbin /SUBSYSTEM:WINDOWS` so no console opens, then the signature.
336
+ Windows 11 24H2 also has a manifest setting that detaches the console when
337
+ launched from Explorer and keeps it in a terminal. Ship the bridge beside
338
+ the executable and name it with `REACT_X11_WIN32_PATH`.
339
+ - **`bun build --compile`** has `--windows-hide-console` and icon and
340
+ metadata flags, but no way to supply a manifest; `bun.exe`'s own manifest
341
+ has no compatibility section, and what that changes for DPI and DWM is not
342
+ yet verified.
343
+ - **Distribution**: Authenticode signing, which SmartScreen's reputation
344
+ follows. MSIX package identity is what unlocks the calendar, the
345
+ capability-status API and declared protocol activation.
346
+ - [packaging.md](packaging.md) has no Windows tier yet.
1297
347
 
1298
- ## Open questions
1299
-
1300
- 1. ~~**The bridge's name and language.**~~ Decided: `@windowkit/win32`, in
1301
- the windowkit organization beside `appkit` — named for the API family it
1302
- binds, as `appkit` is; C++ with node-addon-api, Rust the recorded
1303
- alternative (§"The bridge").
1304
- 2. ~~**The backend's name.**~~ Decided: `'win32'`, matching
1305
- `process.platform` and the API family, the way `'cocoa'` names an API
1306
- rather than an OS.
1307
- 3. **Which composition API holds promoted and retained content** — classic
1308
- DirectComposition or the visual layer. Phase 4's probe.
1309
- 4. **The Windows floor.** Windows 10 1809 or later and Windows 11 —
1310
- per-monitor-v2 needs 1703, the visual layer's desktop target 1803 — with
1311
- the Windows 11 APIs (the compositor clock and its statistics, the DWM
1312
- attributes, the system backdrop) as rungs rather than requirements; or
1313
- Windows 11 only, which simplifies the frame clock and the photon
1314
- measurement. Windows 10 reached end of support in October 2025, with
1315
- paid extended updates after it.
1316
- 5. **Text metrics across backends** — DirectWrite's, the default, or ntk's
1317
- shared ones: macos.md's open question 5, sharper with a third backend.
1318
- 6. ~~**Accessibility**~~ — built as a pushed mirror served by a provider of
1319
- our own (`windows/src/uia.cc`), rather than AccessKit's adapter or a
1320
- provider answering from the live tree (§Accessibility).
1321
- 7. **A drag region in the API.** A drawn titlebar has to tell the platform
1322
- which boxes are caption and which are buttons, for moving, snapping and
1323
- Snap Layouts; X11 (`_NET_WM_MOVERESIZE`), Wayland (`xdg_toplevel.move`)
1324
- and macOS have the same need. One cross-backend declaration on a
1325
- `<box>`, not a Windows-only prop.
1326
- 8. **Live per-window scale** is decided — Windows cannot ship without it —
1327
- but its shape is not: `useScale()` becoming per-window and live is a
1328
- public API change on every backend (§Layout).
1329
- 9. **`x11-dri`'s name, again.** If `<glarea>` on Windows is ANGLE behind
1330
- the same GLES API, the display-server-neutral GL addon wayland.md's open
1331
- question 4 anticipated gains a third platform. It shipped on WGL inside
1332
- `@windowkit/win32` instead ([windows-gl.md](windows-gl.md)), a second GL
1333
- binding beside `x11-dri` rather than a third platform inside it.
1334
-
1335
- ## References
1336
-
1337
- Microsoft documentation:
1338
-
1339
- - DirectComposition: [basic concepts](https://learn.microsoft.com/en-us/windows/win32/directcomp/basic-concepts)
1340
- (threading, cross-device trees),
1341
- [`IDCompositionSurface::BeginDraw`](https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-begindraw),
1342
- [`IDCompositionSurface::Scroll`](https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-scroll),
1343
- [`CreateTargetForHwnd`](https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createtargetforhwnd),
1344
- [animation](https://learn.microsoft.com/en-us/windows/win32/directcomp/animation),
1345
- [the compositor clock](https://learn.microsoft.com/en-us/windows/win32/directcomp/compositor-clock/compositor-clock).
1346
- - DXGI: [`DXGI_SWAP_EFFECT`](https://learn.microsoft.com/en-us/windows/win32/api/dxgi/ne-dxgi-dxgi_swap_effect),
1347
- [`DXGI_PRESENT_PARAMETERS`](https://learn.microsoft.com/en-us/windows/win32/api/dxgi1_2/ns-dxgi1_2-dxgi_present_parameters),
1348
- [the composition swapchain](https://learn.microsoft.com/en-us/windows/win32/comp_swapchain/comp-swapchain),
1349
- [`DCompositionCreateSurfaceHandle`](https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatesurfacehandle).
1350
- - The visual layer: [using it with Win32](https://learn.microsoft.com/en-us/windows/uwp/composition/using-the-visual-layer-with-win32),
1351
- [composition native interop](https://learn.microsoft.com/en-us/windows/apps/develop/composition/composition-native-interop).
1352
- - Direct2D and DirectWrite: [pixel formats and alpha modes](https://learn.microsoft.com/en-us/windows/win32/direct2d/supported-pixel-formats-and-alpha-modes)
1353
- (ClearType), [primitive blend](https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/ne-d2d1_1-d2d1_primitive_blend),
1354
- [multithreaded Direct2D](https://learn.microsoft.com/en-us/windows/win32/direct2d/multi-threaded-direct2d-apps),
1355
- [`IDWriteTextLayout`](https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritetextlayout).
1356
- - Input: [`EnableMouseInPointer`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enablemouseinpointer),
1357
- [precision touchpad devices](https://learn.microsoft.com/en-us/windows/win32/w8cookbook/windows-precision-touchpad-devices),
1358
- [`ITextStoreACP::RequestLock`](https://learn.microsoft.com/en-us/windows/win32/api/textstor/nf-textstor-itextstoreacp-requestlock),
1359
- [input method requirements](https://learn.microsoft.com/en-us/windows/apps/develop/input/input-method-editor-requirements).
1360
- - Windows: [high DPI improvements for desktop applications](https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-improvements-for-desktop-applications)
1361
- (mixed-mode DPI), [custom window frames](https://learn.microsoft.com/en-us/windows/win32/dwm/customframe),
1362
- [Snap Layouts](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/ui/apply-snap-layout-menu),
1363
- [`DWMWINDOWATTRIBUTE`](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute),
1364
- [`AllowSetForegroundWindow`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-allowsetforegroundwindow).
1365
- - Services: [`ProviderOptions`](https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/ne-uiautomationcore-provideroptions),
1366
- [`RegisterDragDrop`](https://learn.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-registerdragdrop),
1367
- [using the clipboard](https://learn.microsoft.com/en-us/windows/win32/dataxchg/using-the-clipboard),
1368
- [sending a desktop toast](https://learn.microsoft.com/en-us/windows/win32/shell/quickstart-sending-desktop-toast),
1369
- [`AppCapability`](https://learn.microsoft.com/en-us/uwp/api/windows.security.authorization.appcapabilityaccess.appcapability),
1370
- [the Windows App SDK 2.0 release notes](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/release-notes/windows-app-sdk-2-0).
1371
- - Raymond Chen on [cross-process parent and child windows](https://devblogs.microsoft.com/oldnewthing/20130412-00/?p=4683)
1372
- and on [thread messages in modal loops](https://devblogs.microsoft.com/oldnewthing/20050426-18/?p=35783).
1373
-
1374
- Source code:
1375
-
1376
- - Electron [`shell/common/node_bindings_win.cc`](https://github.com/electron/electron/blob/main/shell/common/node_bindings_win.cc);
1377
- NodeGui's [`qode` integration](https://github.com/nodegui/qode/blob/master/src/integration/node_integration_win.cc);
1378
- libuv's [loop documentation](https://github.com/libuv/libuv/blob/v1.x/docs/src/loop.rst)
1379
- (`uv_run` is not re-entrant); Node's [`src/node_api.cc`](https://github.com/nodejs/node/blob/main/src/node_api.cc)
1380
- (threadsafe functions) and [`src/res/node.exe.extra.manifest`](https://github.com/nodejs/node/blob/main/src/res/node.exe.extra.manifest).
1381
- - Chromium: [`ui/gl/child_window_win.cc`](https://source.chromium.org/chromium/chromium/src/+/main:ui/gl/child_window_win.cc),
1382
- [`ui/gl/dc_layer_tree.cc`](https://source.chromium.org/chromium/chromium/src/+/main:ui/gl/dc_layer_tree.cc),
1383
- [`ui/gl/vsync_thread_win.cc`](https://source.chromium.org/chromium/chromium/src/+/main:ui/gl/vsync_thread_win.cc),
1384
- [`ui/views/win/hwnd_message_handler.cc`](https://github.com/chromium/chromium/blob/main/ui/views/win/hwnd_message_handler.cc).
1385
- - Firefox [`gfx/webrender_bindings/DCLayerTree.cpp`](https://searchfox.org/mozilla-central/source/gfx/webrender_bindings/DCLayerTree.cpp);
1386
- Flutter's [`flutter_windows_view.cc`](https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc)
1387
- (the resize wait); winit's [`keyboard.rs`](https://github.com/rust-windowing/winit/blob/master/winit-win32/src/keyboard.rs);
1388
- GLFW's [`win32_window.c`](https://github.com/glfw/glfw/blob/master/src/win32_window.c).
1389
- - [AccessKit](https://github.com/AccessKit/accesskit) and
1390
- [its C bindings](https://github.com/AccessKit/accesskit-c);
1391
- [`winpane`](https://github.com/peteretelej/winpane);
1392
- react-native-windows' [new architecture](https://microsoft.github.io/react-native-windows/docs/new-architecture);
1393
- [`node-usb-detection`](https://github.com/MadLittleMods/node-usb-detection)
1394
- and [`uiohook-napi`](https://github.com/SnosMe/uiohook-napi), message
1395
- threads inside stock Node.
348
+ ## Testing
1396
349
 
1397
- In this repository: [wayland.md](wayland.md) and [macos.md](macos.md),
1398
- whose inventories this reuses; [scale.md](scale.md),
1399
- [events.md](events.md), [accessibility.md](accessibility.md) and
1400
- [architecture/accessibility.md](architecture/accessibility.md),
1401
- [clipboard.md](clipboard.md), [drag-and-drop.md](drag-and-drop.md),
1402
- [uri-schemes.md](uri-schemes.md), [desktop.md](desktop.md) and
1403
- [packaging.md](packaging.md) — the per-feature contracts this plan maps
1404
- onto Windows.
350
+ - `react-x11/test` runs a tree against an in-process X server or the mock,
351
+ on Windows as anywhere ([testing.md](testing.md)) — it exercises the X11
352
+ backend, not this one.
353
+ - `window.snapshot()` reads a composed window back on a real Windows desktop
354
+ (above).
355
+ - Test at **fractional scales** — 1.25 and 1.5, not only 1 and 2. Logical and
356
+ device pixels part company there, and a test at scale 1 cannot tell which
357
+ of the two a coordinate is in.
358
+ - GitHub-hosted Windows runners have an interactive desktop (1024×768) and no
359
+ GPU driver, so `<glarea>` reports its error there; the rest of the backend
360
+ on Direct3D's software device (WARP) is not yet verified on them.
361
+
362
+ ## Not supported yet
363
+
364
+ - Live per-window scale: a window on a monitor at another scale is not
365
+ re-laid out ([Scale and DPI](#scale-and-dpi)).
366
+ - UI Automation text ranges: no caret or word navigation inside a field.
367
+ - TSF: voice typing, handwriting and shape-writing; IME reconversion.
368
+ - `decorations={false}`, `transientFor`, and a drag-region API for drawn
369
+ titlebars (snapping, Snap Layouts).
370
+ - Clipboard formats other than text: images, HTML, file lists.
371
+ - File associations, URL schemes, single instance (`useAppOpen`,
372
+ `useAppActivate`); `useLauncherMenu`.
373
+ - Toasts with actions or click events (notifications are balloons).
374
+ - `useEyedropper()`; the calendar without package identity.
375
+ - `<glarea>` without a vendor OpenGL driver (ANGLE).
376
+ - `<video src>`.
377
+ - Compositor-side animation: transitions run on the frame clock, so a long
378
+ JS block stalls them, unlike on Cocoa.
379
+ - Pen- and touch-specific input, touchpad inertia and pinch.
380
+ - `<foreign>`, OLE embedding, window managers and `ssh -X` — by design.