react-x11 2.16.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +38 -23
  2. package/package.json +3 -1
  3. package/src/Reconciler.js +82 -23
  4. package/src/a11y.js +18 -1
  5. package/src/acceleratorhooks.js +40 -6
  6. package/src/anchor.js +20 -2
  7. package/src/appcontext.js +8 -0
  8. package/src/appearance.js +36 -0
  9. package/src/{cocoa → backend}/context2d.js +27 -7
  10. package/src/capabilities.js +99 -1
  11. package/src/cocoa/app.js +204 -6
  12. package/src/cocoa/fonts.js +1 -1
  13. package/src/cocoa/overlay.js +2 -2
  14. package/src/cocoa/panewindow.js +2 -2
  15. package/src/cocoa/presenter.js +2 -2
  16. package/src/cocoa/surface.js +3 -3
  17. package/src/cocoa/window.js +2 -2
  18. package/src/events.js +21 -0
  19. package/src/foreignnodes.js +8 -3
  20. package/src/frame/index.js +30 -4
  21. package/src/glnodes.js +12 -1
  22. package/src/idle.js +59 -1
  23. package/src/index.d.ts +51 -1
  24. package/src/index.js +30 -3
  25. package/src/keysymchars.js +47 -0
  26. package/src/keysyms.d.ts +19 -1
  27. package/src/keysyms.js +107 -8
  28. package/src/launcher.js +17 -8
  29. package/src/launcherhooks.js +24 -10
  30. package/src/node.d.ts +1 -1
  31. package/src/nodes/cascade.js +9 -0
  32. package/src/nodes/node.js +6 -1
  33. package/src/nodes/window/hints.js +21 -2
  34. package/src/nodes/window/window.js +2 -2
  35. package/src/notifications.js +39 -14
  36. package/src/screens.js +159 -24
  37. package/src/taskbarhooks.js +164 -0
  38. package/src/transfer.js +20 -1
  39. package/src/trayhooks.js +1 -1
  40. package/src/types/capabilities.d.ts +32 -3
  41. package/src/types/elements.d.ts +23 -1
  42. package/src/types/events.d.ts +21 -0
  43. package/src/types/filedialog.d.ts +3 -1
  44. package/src/types/launcher.d.ts +20 -6
  45. package/src/types/taskbar.d.ts +79 -0
  46. package/src/wayland/context2d.js +1 -1
  47. package/src/wayland/xkb.js +170 -59
  48. package/src/win32/a11y.js +604 -0
  49. package/src/win32/app.js +768 -0
  50. package/src/win32/bezels.js +158 -0
  51. package/src/win32/dnd.js +283 -0
  52. package/src/win32/fonts.js +497 -0
  53. package/src/win32/glarea.js +548 -0
  54. package/src/win32/ime.js +267 -0
  55. package/src/win32/keymap.js +116 -0
  56. package/src/win32/native.js +54 -0
  57. package/src/win32/panehost.js +106 -0
  58. package/src/win32/panewindow.js +343 -0
  59. package/src/win32/shell.js +426 -0
  60. package/src/win32/surface.js +192 -0
  61. package/src/win32/window.js +659 -0
  62. package/src/windowid.js +128 -20
@@ -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
+ }