react-x11 2.16.1 → 2.17.1
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 +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +17 -9
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/glarea.js +48 -10
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +23 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +21 -7
- package/src/idle.js +59 -1
- package/src/index.d.ts +41 -0
- package/src/index.js +30 -3
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +16 -0
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +66 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// The pane process's "window" on the Windows backend — docs/frame.md's child
|
|
2
|
+
// half, with the composition engine standing in for the X server.
|
|
3
|
+
//
|
|
4
|
+
// On X11 a pane renders into a real window of its own and the host reparents
|
|
5
|
+
// it. There is no reparenting here, so the two processes share a **buffer**:
|
|
6
|
+
// the pane makes a DirectComposition surface handle, hangs a composition
|
|
7
|
+
// swapchain off it, and draws through the same verb table a window draws
|
|
8
|
+
// through; the host opens that handle and gives it to a visual of its own
|
|
9
|
+
// (src/win32/panehost.js). See docs/windows-embedding.md for why this rather
|
|
10
|
+
// than a shared DXGI texture — in one line, nothing here has to synchronise.
|
|
11
|
+
//
|
|
12
|
+
// **The present is the hand-off.** Once the host holds the handle the
|
|
13
|
+
// compositor scans out of whichever buffer this side presented last, so a
|
|
14
|
+
// frame costs no message, no fence and no copy. That is the one real
|
|
15
|
+
// difference from the Cocoa pane, which names a fresh IOSurface every frame;
|
|
16
|
+
// here `pane-present` carries the same handle every time and is therefore
|
|
17
|
+
// sent only when the host can be believed not to have it (`_publish`).
|
|
18
|
+
//
|
|
19
|
+
// What the node tree sees is the ordinary window contract — the Win32Window
|
|
20
|
+
// one, `presentFrame` and all, because a pane paints the way a window does.
|
|
21
|
+
// Geometry and input arrive as channel messages: the host owns layout and
|
|
22
|
+
// hit-testing, which is what makes this CPU offloading rather than isolation.
|
|
23
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
24
|
+
|
|
25
|
+
// An id that is recognisably not an HWND, for the same reason the Cocoa pane
|
|
26
|
+
// has one: the ready handshake polls `windowIdOf`, which wants a number, and
|
|
27
|
+
// nothing on the host's side ever dereferences it. 'dc' for the composition
|
|
28
|
+
// device the buffer belongs to.
|
|
29
|
+
let nextPaneWindow = 1;
|
|
30
|
+
const PANE_ID_BASE = 0xdc0a0000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How many frames back a flip chain's back buffer is — and therefore how
|
|
34
|
+
* much of the past a partial repaint has to cover.
|
|
35
|
+
*
|
|
36
|
+
* A flip chain does not hand out a persistent bitmap. `GetBuffer(0)` after a
|
|
37
|
+
* present is the buffer that was on screen two presents ago, so a frame that
|
|
38
|
+
* repaints only its own damage leaves the rest of the pane showing frame
|
|
39
|
+
* N-2: every partial frame would jump two frames back and then forward
|
|
40
|
+
* again. Painting this frame's damage *and* the previous frame's brings the
|
|
41
|
+
* buffer current, which is the standard accumulation for a flip chain and is
|
|
42
|
+
* why this number is the chain's depth rather than a tuning knob.
|
|
43
|
+
*
|
|
44
|
+
* Measured, not assumed: with five distinct frames presented and the fifth
|
|
45
|
+
* painting only a corner, the rest of the pane came back as **frame three**.
|
|
46
|
+
* The bridge's test/pane.js holds it there, because a deeper chain would
|
|
47
|
+
* turn this into ghosting nobody would think to look for.
|
|
48
|
+
*/
|
|
49
|
+
const STALE_FRAMES = 2;
|
|
50
|
+
|
|
51
|
+
/** The smallest rect covering every list given, or null for "everything". */
|
|
52
|
+
function boundsOf(lists) {
|
|
53
|
+
let x0 = Infinity;
|
|
54
|
+
let y0 = Infinity;
|
|
55
|
+
let x1 = -Infinity;
|
|
56
|
+
let y1 = -Infinity;
|
|
57
|
+
for (const rects of lists) {
|
|
58
|
+
if (!rects) return null; // one unbounded frame makes the union unbounded
|
|
59
|
+
for (const r of rects) {
|
|
60
|
+
if (r.x < x0) x0 = r.x;
|
|
61
|
+
if (r.y < y0) y0 = r.y;
|
|
62
|
+
if (r.x + r.width > x1) x1 = r.x + r.width;
|
|
63
|
+
if (r.y + r.height > y1) y1 = r.y + r.height;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!(x1 > x0 && y1 > y0)) return null;
|
|
67
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class Win32PaneWindow {
|
|
71
|
+
constructor(app, attributes = {}) {
|
|
72
|
+
this.app = app;
|
|
73
|
+
this._native = app._native;
|
|
74
|
+
this.attributes = attributes;
|
|
75
|
+
this.destroyed = false;
|
|
76
|
+
this.mapped = false;
|
|
77
|
+
this.parent = null;
|
|
78
|
+
this.cursor = null;
|
|
79
|
+
this.x = 0;
|
|
80
|
+
this.y = 0;
|
|
81
|
+
this.title = attributes.title ?? '';
|
|
82
|
+
|
|
83
|
+
// Device pixels on both sides of this boundary, as everywhere else on
|
|
84
|
+
// this backend. The host sends its rect in logical units with the scale
|
|
85
|
+
// it measured, and `setPaneSize` does the multiply.
|
|
86
|
+
this.scale = app.scale ?? 1;
|
|
87
|
+
this.width = Math.max(1, Math.round(attributes.width ?? 400));
|
|
88
|
+
this.height = Math.max(1, Math.round(attributes.height ?? 300));
|
|
89
|
+
|
|
90
|
+
this._handlers = {};
|
|
91
|
+
this._surface = 0;
|
|
92
|
+
this._gen = 0;
|
|
93
|
+
this._ctx = null;
|
|
94
|
+
this._reactX11Node = null;
|
|
95
|
+
// The damage of the frames still standing between the back buffer and
|
|
96
|
+
// now. All null to start: the buffers of a fresh swapchain hold nothing,
|
|
97
|
+
// so the first frames have to be unbounded, and saying so here is what
|
|
98
|
+
// makes `presentFrame`'s induction true from the very first one.
|
|
99
|
+
this._recent = new Array(STALE_FRAMES - 1).fill(null);
|
|
100
|
+
this._owesFull = true;
|
|
101
|
+
this._drawn = false;
|
|
102
|
+
|
|
103
|
+
this.id = PANE_ID_BASE + nextPaneWindow++;
|
|
104
|
+
this.windowId = this.id;
|
|
105
|
+
|
|
106
|
+
// The host's process, so the handle can be duplicated straight into it
|
|
107
|
+
// and `paneAttach` is one step over there. A fork's parent *is* the
|
|
108
|
+
// host; a pane run behind a custom transport has to say which process to
|
|
109
|
+
// publish to, because `ppid` would be whatever launched the runner.
|
|
110
|
+
const hostPid = app.options?.paneHostPid ?? process.ppid;
|
|
111
|
+
const pane = this._native.paneCreate(this.width, this.height, hostPid);
|
|
112
|
+
if (!pane) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'react-x11: this pane could not make a buffer to share with its ' +
|
|
115
|
+
`host (pid ${hostPid}). A pane publishes frames by duplicating a ` +
|
|
116
|
+
'composition surface handle into the host process, which needs ' +
|
|
117
|
+
'the host to be this process’s parent — pass ' +
|
|
118
|
+
'`win32: { paneHostPid }` to createRoot() if it is not.',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
this._pane = pane.id;
|
|
122
|
+
this._handle = pane.handle;
|
|
123
|
+
|
|
124
|
+
app._register(this);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- the channel-facing half ---------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The host's layout answer: the pane's size in logical units, plus the
|
|
131
|
+
* scale of the display the *host* is on — which is the one that matters,
|
|
132
|
+
* since these pixels are composited into the host's window.
|
|
133
|
+
*/
|
|
134
|
+
setPaneSize(width, height, scale) {
|
|
135
|
+
if (this.destroyed) return;
|
|
136
|
+
if (scale) this.scale = scale;
|
|
137
|
+
// The host is listening, which is the other half of `_publish`: a
|
|
138
|
+
// pane-rect is the one message that arrives *after* the host has built
|
|
139
|
+
// its pane view and subscribed, so it is the moment to say again which
|
|
140
|
+
// buffer to show. It costs one message per resize, and it is what makes
|
|
141
|
+
// a present that raced the host's subscription recoverable instead of a
|
|
142
|
+
// pane that stays blank for good.
|
|
143
|
+
this._publish();
|
|
144
|
+
const w = Math.max(1, Math.round(width * this.scale));
|
|
145
|
+
const h = Math.max(1, Math.round(height * this.scale));
|
|
146
|
+
if (w === this.width && h === this.height) return;
|
|
147
|
+
this.width = w;
|
|
148
|
+
this.height = h;
|
|
149
|
+
if (!this._native.paneResize(this._pane, w, h)) return;
|
|
150
|
+
// Fresh buffers, undefined contents — the same standing start the
|
|
151
|
+
// constructor sets up, for the same reason.
|
|
152
|
+
this._owesFull = true;
|
|
153
|
+
this._recent.fill(null);
|
|
154
|
+
this.emit('resize', {
|
|
155
|
+
width: w,
|
|
156
|
+
height: h,
|
|
157
|
+
x: 0,
|
|
158
|
+
y: 0,
|
|
159
|
+
moved: false,
|
|
160
|
+
resized: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Tell the host which buffer is this pane's, once there is a frame in it.
|
|
165
|
+
* Idempotent on the far side — `Win32PaneHost.present` attaches the first
|
|
166
|
+
* time and recognises the handle every time after. */
|
|
167
|
+
_publish() {
|
|
168
|
+
if (this.destroyed || !this._drawn) return;
|
|
169
|
+
this.app._paneSend?.({
|
|
170
|
+
type: 'pane-present',
|
|
171
|
+
id: this._handle,
|
|
172
|
+
width: this.width,
|
|
173
|
+
height: this.height,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- the window contract -------------------------------------------------
|
|
178
|
+
|
|
179
|
+
on(name, fn) {
|
|
180
|
+
(this._handlers[name] ??= []).push(fn);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
off(name, fn) {
|
|
184
|
+
const list = this._handlers[name];
|
|
185
|
+
if (!list) return;
|
|
186
|
+
const at = list.indexOf(fn);
|
|
187
|
+
if (at >= 0) list.splice(at, 1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
emit(name, ev) {
|
|
191
|
+
for (const fn of this._handlers[name] ?? []) fn(ev);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
map() {
|
|
195
|
+
this.mapped = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
unmap() {
|
|
199
|
+
this.mapped = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
focus() {}
|
|
203
|
+
|
|
204
|
+
setTitle(title) {
|
|
205
|
+
this.title = title;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setSizeHints() {}
|
|
209
|
+
|
|
210
|
+
/** The host sizes a pane, not the pane itself: a `<Frame>`'s box is laid
|
|
211
|
+
* out over there. A tree inside the pane asking to grow is answered by
|
|
212
|
+
* the size the host sends back through `setPaneSize`. */
|
|
213
|
+
resize() {}
|
|
214
|
+
|
|
215
|
+
move() {}
|
|
216
|
+
|
|
217
|
+
grabKeyboard(options, cb) {
|
|
218
|
+
cb?.(null, 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
ungrabKeyboard() {}
|
|
222
|
+
|
|
223
|
+
selectXI2() {
|
|
224
|
+
return Promise.resolve(false);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
requestAnimationFrame(cb) {
|
|
228
|
+
return this.app._requestFrame(cb, this);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Nothing to wait for: the composition engine keeps the last presented
|
|
232
|
+
* buffer and the pane owns the next one outright, so a frame is never in
|
|
233
|
+
* flight in the sense the pacing gate means — the same answer
|
|
234
|
+
* Win32Window gives, for the same reason. */
|
|
235
|
+
frameInFlight() {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
getContext() {
|
|
240
|
+
if (this._ctx) return this._ctx;
|
|
241
|
+
this._ctx = new BackendContext2D(
|
|
242
|
+
this._native,
|
|
243
|
+
() => this._surface,
|
|
244
|
+
() => this._gen,
|
|
245
|
+
);
|
|
246
|
+
this._ctx._fonts = this.app.fonts;
|
|
247
|
+
return this._ctx;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The frame: one `BeginDraw` over the buffer, one `Present` for the lot.
|
|
252
|
+
*
|
|
253
|
+
* Unlike a window, a pane cannot take its damage rect by rect — a flip
|
|
254
|
+
* chain hands out one whole back buffer, not a tile per update — so the
|
|
255
|
+
* damage becomes a single clipped pass over the union of this frame's
|
|
256
|
+
* rects and the previous frame's (see `STALE_FRAMES`). It is the union's
|
|
257
|
+
* *bounding box* rather than the rects themselves, because a second pass
|
|
258
|
+
* over a pixel a first pass already painted composites onto it twice, and
|
|
259
|
+
* a translucent node painted twice is the wrong colour.
|
|
260
|
+
*/
|
|
261
|
+
presentFrame(node, damage) {
|
|
262
|
+
if (this.destroyed) return;
|
|
263
|
+
if (process.env.REACT_X11_WIN32_FULL_REPAINT === '1') damage = null;
|
|
264
|
+
if (this._owesFull) {
|
|
265
|
+
this._owesFull = false;
|
|
266
|
+
damage = null;
|
|
267
|
+
}
|
|
268
|
+
const rect = boundsOf([damage, ...this._recent]);
|
|
269
|
+
this._recent.unshift(damage);
|
|
270
|
+
this._recent.length = STALE_FRAMES - 1;
|
|
271
|
+
|
|
272
|
+
const surface = this._native.paneBeginDraw(this._pane);
|
|
273
|
+
if (!surface) return;
|
|
274
|
+
this._surface = surface;
|
|
275
|
+
this._gen++;
|
|
276
|
+
try {
|
|
277
|
+
// `_paintRegion` clips to the rect it is given, so the pass is bounded
|
|
278
|
+
// by exactly what it was handed; a null rect is the whole pane.
|
|
279
|
+
node._paintRegion(this.getContext(), rect, this.width, this.height);
|
|
280
|
+
} finally {
|
|
281
|
+
this._surface = 0;
|
|
282
|
+
this._native.paneEndDraw(this._pane);
|
|
283
|
+
}
|
|
284
|
+
if (!this._drawn) {
|
|
285
|
+
this._drawn = true;
|
|
286
|
+
this._publish();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* No scroll blit. The fast path moves a band of pixels *within the surface
|
|
292
|
+
* being drawn into*, and this one is `STALE_FRAMES` frames behind — the
|
|
293
|
+
* band it would move is the wrong picture, shifted. Answering false is not
|
|
294
|
+
* a gap: the frame falls back to repainting the scrolled region, which is
|
|
295
|
+
* what the union pass above does anyway.
|
|
296
|
+
*/
|
|
297
|
+
scrollRegion() {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
present() {
|
|
302
|
+
// Nothing to flip: presentFrame presented. Kept because the frame loop
|
|
303
|
+
// calls it on every window it paced.
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
snapshot() {
|
|
307
|
+
return false; // no window of our own; the host's is where these show
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* What another process embeds to show this window — `windowHandleOf`'s
|
|
312
|
+
* answer here (src/windowid.js).
|
|
313
|
+
*
|
|
314
|
+
* On X11 that is the window's own id, because an XID means the same thing
|
|
315
|
+
* in every process on the display. Windows has no such number: a window
|
|
316
|
+
* cannot be embedded at all, because a composition target stops presenting
|
|
317
|
+
* the moment its window becomes a child (docs/windows-embedding.md, with
|
|
318
|
+
* the measurements). What crosses instead is the **buffer** — the same
|
|
319
|
+
* composition surface handle a `<Frame>` pane publishes — and the host
|
|
320
|
+
* binds it to a visual of its own rather than reparenting anything.
|
|
321
|
+
*
|
|
322
|
+
* The handle is already valid in the host process: it was duplicated there
|
|
323
|
+
* when the pane was made. Which host that is, is `paneHostPid` — the
|
|
324
|
+
* parent by default, because a host starts its guest.
|
|
325
|
+
*/
|
|
326
|
+
embedHandle() {
|
|
327
|
+
return this.destroyed ? null : this._handle;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
destroy() {
|
|
331
|
+
if (this.destroyed) return;
|
|
332
|
+
this.destroyed = true;
|
|
333
|
+
this._native.paneDestroy(this._pane);
|
|
334
|
+
this.app._unregister(this);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The union collapses to one box, so a longer damage list buys a tighter
|
|
338
|
+
* bound and never another pass. Sixteen is the window's, kept so the two
|
|
339
|
+
* paths are handed the same lists. */
|
|
340
|
+
get damageRectCap() {
|
|
341
|
+
return 16;
|
|
342
|
+
}
|
|
343
|
+
}
|