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.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
It
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
538
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
What
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
**
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
from `onMouseMove
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
the
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
the app's own with `rcedit`, the GUI subsystem with
|
|
1180
|
-
`editbin /SUBSYSTEM:WINDOWS
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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.
|