react-x11 2.16.1 → 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 (54) 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/appcontext.js +8 -0
  6. package/src/appearance.js +36 -0
  7. package/src/{cocoa → backend}/context2d.js +27 -7
  8. package/src/capabilities.js +99 -1
  9. package/src/cocoa/app.js +8 -4
  10. package/src/cocoa/fonts.js +1 -1
  11. package/src/cocoa/overlay.js +2 -2
  12. package/src/cocoa/panewindow.js +2 -2
  13. package/src/cocoa/presenter.js +2 -2
  14. package/src/cocoa/surface.js +3 -3
  15. package/src/cocoa/window.js +2 -2
  16. package/src/events.js +21 -0
  17. package/src/foreignnodes.js +8 -3
  18. package/src/frame/index.js +30 -4
  19. package/src/glnodes.js +12 -1
  20. package/src/idle.js +59 -1
  21. package/src/index.d.ts +41 -0
  22. package/src/index.js +30 -3
  23. package/src/launcher.js +17 -8
  24. package/src/launcherhooks.js +24 -10
  25. package/src/node.d.ts +1 -1
  26. package/src/nodes/cascade.js +9 -0
  27. package/src/nodes/node.js +6 -1
  28. package/src/nodes/window/hints.js +21 -2
  29. package/src/nodes/window/window.js +2 -2
  30. package/src/notifications.js +39 -14
  31. package/src/taskbarhooks.js +164 -0
  32. package/src/transfer.js +20 -1
  33. package/src/trayhooks.js +1 -1
  34. package/src/types/capabilities.d.ts +32 -3
  35. package/src/types/elements.d.ts +23 -1
  36. package/src/types/events.d.ts +16 -0
  37. package/src/types/launcher.d.ts +20 -6
  38. package/src/types/taskbar.d.ts +79 -0
  39. package/src/wayland/context2d.js +1 -1
  40. package/src/win32/a11y.js +604 -0
  41. package/src/win32/app.js +768 -0
  42. package/src/win32/bezels.js +158 -0
  43. package/src/win32/dnd.js +283 -0
  44. package/src/win32/fonts.js +497 -0
  45. package/src/win32/glarea.js +548 -0
  46. package/src/win32/ime.js +267 -0
  47. package/src/win32/keymap.js +116 -0
  48. package/src/win32/native.js +54 -0
  49. package/src/win32/panehost.js +106 -0
  50. package/src/win32/panewindow.js +343 -0
  51. package/src/win32/shell.js +426 -0
  52. package/src/win32/surface.js +192 -0
  53. package/src/win32/window.js +659 -0
  54. package/src/windowid.js +66 -0
@@ -0,0 +1,659 @@
1
+ // An ntk-window-shaped object over an HWND — the contract WindowNode realizes
2
+ // against (src/testing/mock-app.js is the reference shape; this file is that
3
+ // shape with DirectComposition behind it).
4
+ //
5
+ // Units: everything crossing this object's boundary is device pixels, like an
6
+ // X window. The window is per-monitor-v2 on the bridge's UI thread, so the
7
+ // HWND's own client area is already in device pixels and there is no divide.
8
+ //
9
+ // The frame is taken through `presentFrame` rather than through `getContext` +
10
+ // `present`, and that is the one structural difference from the Cocoa window.
11
+ // A DirectComposition surface is not a persistent bitmap that gets flipped: it
12
+ // hands back a drawing context per `BeginDraw`, valid until `EndDraw`, and
13
+ // every pixel inside the rect is repainted while every pixel outside it is
14
+ // kept. So the frame has to be *driven* from the damage list rather than
15
+ // painted into a surface that was already there — which `presentFrame` is
16
+ // exactly the hook for, and which makes one BeginDraw per damage rect fall out
17
+ // for free.
18
+ import { BackendContext2D } from '../backend/context2d.js';
19
+ import { Win32DropTransport, dragSpec } from './dnd.js';
20
+
21
+ // REACT_X11_WIN32_DEBUG=1 reports every frame and what it drew into. A window
22
+ // that stays blank on this backend has a short list of causes — no frame
23
+ // asked for, a BeginDraw refused, a damage rect outside the surface — and they
24
+ // are indistinguishable from the outside, because none of them throws.
25
+ const DEBUG = process.env.REACT_X11_WIN32_DEBUG === '1';
26
+
27
+ export class Win32Window {
28
+ constructor(app, attributes = {}) {
29
+ this.app = app;
30
+ this._native = app._native;
31
+ this.attributes = attributes;
32
+
33
+ this.width = Math.max(1, Math.round(attributes.width ?? 800));
34
+ this.height = Math.max(1, Math.round(attributes.height ?? 600));
35
+ this.x = attributes.x ?? 0;
36
+ this.y = attributes.y ?? 0;
37
+ this.title = attributes.title ?? '';
38
+ this.mapped = false;
39
+ this.destroyed = false;
40
+ this.parent = null;
41
+ this.cursor = null;
42
+
43
+ // The handle the verb table draws through, non-zero only inside a
44
+ // BeginDraw/EndDraw pair, and the generation that tells BackendContext2D
45
+ // its sticky state has to be pushed into a fresh one.
46
+ this._surface = 0;
47
+ this._gen = 0;
48
+ this._ctx = null;
49
+
50
+ this._handlers = {};
51
+ this._ready = false;
52
+ this._composed = false;
53
+ this._pendingFrames = [];
54
+ this._owesFullPaint = false;
55
+ this._dirty = false;
56
+
57
+ // `overrideRedirect` is how a `<popup>` says what it is — a menu, a
58
+ // select's list, a tooltip. On X11 it means "the window manager does not
59
+ // manage this"; here it means WS_POPUP: no frame, no taskbar button, and
60
+ // no stealing activation from the window it belongs to.
61
+ this.popup = attributes.overrideRedirect === true;
62
+
63
+ this.id = this._native.createWindow({
64
+ title: this.title,
65
+ width: this.width,
66
+ height: this.height,
67
+ // A popup is placed by anchor.js against the monitor's work area, and
68
+ // that placement *is* the contract — a menu created at the default
69
+ // position is a menu in the wrong place.
70
+ x: attributes.x,
71
+ y: attributes.y,
72
+ popup: this.popup,
73
+ // A shaped window needs a surface with an alpha channel, or the pixels
74
+ // outside its shape composite as a dark fringe — which on a rounded
75
+ // popup is a dark edge along every corner.
76
+ transparent: attributes.transparent === true,
77
+ // A `<popup dragPreview>` follows the pointer, so it is **under** the
78
+ // pointer for the whole gesture — and the window under the pointer is
79
+ // the one the shell asks about when it looks for somewhere to drop.
80
+ // Unanswered, the preview answers for itself, and it is not a drop
81
+ // target: the list underneath never sees the drop. Cocoa spells the
82
+ // same thing `ignoresMouseEvents` (src/cocoa/window.js).
83
+ clickThrough: attributes.dragPreview === true,
84
+ });
85
+ app._register(this);
86
+ }
87
+
88
+ // --- events --------------------------------------------------------------
89
+
90
+ on(name, fn) {
91
+ (this._handlers[name] ??= []).push(fn);
92
+ }
93
+
94
+ /** The other half of `on`. Without it a component that subscribes for as
95
+ * long as it is mounted has no way to stop, and the handler outlives it. */
96
+ off(name, fn) {
97
+ const list = this._handlers[name];
98
+ if (!list) return;
99
+ const at = list.indexOf(fn);
100
+ if (at >= 0) list.splice(at, 1);
101
+ }
102
+
103
+ emit(name, ev) {
104
+ for (const fn of this._handlers[name] ?? []) fn(ev);
105
+ }
106
+
107
+ /**
108
+ * The bridge says the HWND exists. Composition is set up here and not at
109
+ * construction because there is no HWND to target until now — the command
110
+ * queue is one way and a window is created asynchronously.
111
+ *
112
+ * Then `draw`, which is the frame clock's "the backing store is invalid,
113
+ * repaint everything" and is what the window node listens to. It is not
114
+ * optional: the tree mounts, lays out and paints in the same turn the window
115
+ * is asked for, so the first frames land before there is anything to paint
116
+ * into and are dropped. Without this the window stays whatever
117
+ * WS_EX_NOREDIRECTIONBITMAP shows when nothing was ever committed, which is
118
+ * black, and nothing else would ever ask again.
119
+ */
120
+ _onReady(originX, originY) {
121
+ this._ready = true;
122
+ // The HWND exists now, which is the first moment a drop target can be
123
+ // registered on it — the tree mounted its `dropAccept`s before this.
124
+ this._dropTransport?.reattach();
125
+ // And the first moment there is a window for the accessibility mirror to
126
+ // be keyed by. The tree mounted and committed before this, so without
127
+ // this the first push would have had nowhere to go and a screen reader
128
+ // attaching to an idle application would find an empty window.
129
+ this.app._a11y?.windowReady(this);
130
+ // The identity goes on **before `show`** below: the taskbar reads the
131
+ // window's AppUserModelID when it makes the button, so an id arriving
132
+ // afterwards leaves that button grouped where it already was.
133
+ if (this.attributes?.appId != null) this.setClass(this.attributes.appId);
134
+ if (Number.isFinite(originX)) this._noteOrigin(originX, originY);
135
+ if (!this._composed) {
136
+ this._native.compose(this.id);
137
+ this._composed = true;
138
+ }
139
+ if (this.mapped) this._native.show(this.id, true);
140
+ this.emit('draw', {});
141
+ }
142
+
143
+ // --- geometry ------------------------------------------------------------
144
+
145
+ /**
146
+ * Where the client area is on the virtual screen. `anchor.js` reads
147
+ * `_screenOrigin` to turn a node's rect into the screen rect a `<popup>` is
148
+ * placed against, and falls back to `x`/`y` — so a window that never reports
149
+ * its position anchors every menu as though it were at the screen's origin,
150
+ * which puts a select's list a whole window-offset away from the select.
151
+ */
152
+ _noteOrigin(x, y) {
153
+ this.x = x;
154
+ this.y = y;
155
+ this._screenOrigin = { x, y };
156
+ }
157
+
158
+ map() {
159
+ this.mapped = true;
160
+ if (this._ready) this._native.show(this.id, true);
161
+ }
162
+
163
+ unmap() {
164
+ this.mapped = false;
165
+ if (this._ready) this._native.show(this.id, false);
166
+ }
167
+
168
+ destroy() {
169
+ if (this.destroyed) return;
170
+ this.destroyed = true;
171
+ this._native.destroyWindow?.(this.id);
172
+ this.app._unregister(this);
173
+ }
174
+
175
+ resize(width, height) {
176
+ this.width = Math.max(1, Math.round(width));
177
+ this.height = Math.max(1, Math.round(height));
178
+ this._native.resizeWindow?.(this.id, this.width, this.height);
179
+ // Unconditionally, not only once composed: an auto-sized window is
180
+ // measured and resized before its HWND exists, and the surface compose()
181
+ // makes later is made at whatever size the bridge last recorded.
182
+ this._native.resize(this.id, this.width, this.height);
183
+ }
184
+
185
+ move(x, y) {
186
+ this.x = x;
187
+ this.y = y;
188
+ this._native.moveWindow?.(this.id, x, y);
189
+ }
190
+
191
+ setTitle(title) {
192
+ this.title = title;
193
+ this._native.setTitle?.(this.id, String(title ?? ''));
194
+ }
195
+
196
+ /** The size limits Windows asks for synchronously in WM_GETMINMAXINFO, so
197
+ * they are pushed ahead rather than answered on demand — docs/windows.md
198
+ * "What Windows asks synchronously, and JS can know in advance". */
199
+ setSizeHints(hints = {}) {
200
+ this._native.setSizeHints?.(this.id, hints);
201
+ }
202
+
203
+ /**
204
+ * `<window appId>`: the AppUserModelID, which is what Windows means by
205
+ * "which application is this" — the taskbar groups buttons by it, pinning
206
+ * pins it, and a jump list belongs to it.
207
+ *
208
+ * X11 carries an instance and a class; this takes the **class**, the half
209
+ * that names the application rather than the window, which is the same
210
+ * choice `windowAttributes` makes for every single-id backend
211
+ * (src/nodes/window/hints.js).
212
+ *
213
+ * Set per window rather than per process:
214
+ * `SetCurrentProcessExplicitAppUserModelID` has to be called before the
215
+ * process creates any UI, which a library cannot promise of an embedder. A
216
+ * window's own id overrides the process's anyway, so this is both the more
217
+ * flexible form and the only one that can be guaranteed.
218
+ */
219
+ setClass(instance, className) {
220
+ const id = className ?? instance;
221
+ this._native.windowAppId?.(this.id, id == null ? null : String(id));
222
+ this._setRelaunch(id == null ? null : String(id));
223
+ }
224
+
225
+ /**
226
+ * What a **pinned** tile starts, and what it is called while pinned.
227
+ *
228
+ * An id on its own is half the story. It makes the taskbar group this
229
+ * window under an identity of its own — and then a user who pins that
230
+ * button gets a shortcut to whatever the shell can work out by itself,
231
+ * which for `node app.js` is node.exe, under node's name and icon. The
232
+ * relaunch properties are the other half, and Microsoft's guidance is that
233
+ * an application setting the id sets these too.
234
+ *
235
+ * Derived rather than asked for, because every part of it is already known
236
+ * and a second Windows-only prop to make the first one work is a bad trade:
237
+ *
238
+ * - **the command** is the one that started this process, quoted — argv as
239
+ * it was, so the relaunch is the launch;
240
+ * - **the name** is the window's title, falling back to the id. It is what
241
+ * the pin menu and the button's tooltip show;
242
+ * - **the icon** is the executable's own, which is what the shell would
243
+ * have used anyway — named explicitly so the pinned tile keeps it rather
244
+ * than resolving it again from a shortcut that may not exist.
245
+ *
246
+ * Clearing the id clears all three: a window with no identity of its own
247
+ * should not keep claiming how to relaunch one.
248
+ */
249
+ _setRelaunch(id) {
250
+ if (typeof this._native.windowRelaunch !== 'function') return;
251
+ if (id == null) {
252
+ this._native.windowRelaunch(this.id, {
253
+ command: null,
254
+ displayName: null,
255
+ icon: null,
256
+ });
257
+ return;
258
+ }
259
+ const quote = (arg) =>
260
+ /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
261
+ const argv = process.argv.slice(1).map(quote).join(' ');
262
+ this._native.windowRelaunch(this.id, {
263
+ command: `${quote(process.execPath)}${argv ? ` ${argv}` : ''}`,
264
+ displayName: this.title || id,
265
+ icon: `${process.execPath},0`,
266
+ });
267
+ }
268
+
269
+ // Still X11's names, still accepted and dropped: each is optional
270
+ // decoration, and an app that sets one should not fail to open a window.
271
+ setWindowType() {}
272
+ setActions() {}
273
+ setTransientFor() {}
274
+ setAlwaysOnTop(on = true) {
275
+ this._native.windowState(this.id, 'above', on !== false);
276
+ }
277
+ setProperty() {
278
+ return Promise.resolve(this);
279
+ }
280
+ deleteProperty() {
281
+ return Promise.resolve(this);
282
+ }
283
+
284
+ /**
285
+ * One of `<window>`'s EWMH state names, applied with the Windows call that
286
+ * means it: `ShowWindow` for maximized and minimized, a remembered frame
287
+ * and the monitor's rect for fullscreen, `SetWindowPos` for `above`.
288
+ *
289
+ * Resolves false for a name this platform has no answer for, which is what
290
+ * `useWindowState()` reads to know the request went nowhere — never for a
291
+ * call that was made and did not take. `focused` is the one that can be
292
+ * refused after the fact: the shell will not let a background process take
293
+ * the foreground, and that refusal is the documented behaviour rather than
294
+ * a failure, so it reports true and the state simply does not change.
295
+ */
296
+ setWmState(names, action = 'add') {
297
+ // A name or a list of them, which is the contract the other backends
298
+ // keep (src/cocoa/window.js) even though the renderer's own caller sends
299
+ // them one at a time.
300
+ const list = Array.isArray(names) ? names : [names];
301
+ // A bridge too old to have the verb answers false for every name rather
302
+ // than throwing: `useWindowState()` reads that false to know the request
303
+ // went nowhere, and a throw from a state a window never had is not a
304
+ // failure an application can do anything with.
305
+ if (typeof this._native.windowState !== 'function') {
306
+ return Promise.resolve(false);
307
+ }
308
+ const held = action === 'toggle' ? new Set(this.getWmStatesNow()) : null;
309
+ let honoured = true;
310
+ for (const name of list) {
311
+ const on = held ? !held.has(name) : action !== 'remove';
312
+ if (!this._native.windowState(this.id, name, on)) honoured = false;
313
+ }
314
+ return Promise.resolve(honoured);
315
+ }
316
+
317
+ /** The live states, synchronously — what a toggle has to read first. */
318
+ getWmStatesNow() {
319
+ if (typeof this._native.windowStates !== 'function') return [];
320
+ return this._native.windowStates(this.id) ?? [];
321
+ }
322
+
323
+ getWmStates() {
324
+ return Promise.resolve(this.getWmStatesNow());
325
+ }
326
+
327
+ // --- drag and drop -------------------------------------------------------
328
+
329
+ /**
330
+ * The window's drop side, which the tree installs when it first mounts a
331
+ * `dropAccept` under this window (src/nodes/window/droptarget.js). Its
332
+ * presence is what tells the tree this backend has drop machinery of its
333
+ * own and no XDND property to write.
334
+ */
335
+ attachDropTransport(session, node) {
336
+ this._dropTransport = new Win32DropTransport(this, session, node);
337
+ }
338
+
339
+ /** A `dropAccept` came or went under this window. The shell registers no
340
+ * types, so all this decides is whether the window is a target at all. */
341
+ dropTargetsChanged() {
342
+ this._dropTransport?.refreshTypes();
343
+ }
344
+
345
+ /** Backend events for the drag in progress, routed by the app. */
346
+ _routeDrag(event) {
347
+ this._dropTransport?.handle(event);
348
+ }
349
+
350
+ /**
351
+ * The source side: hand a `DragSession`'s gesture to the shell. Returns at
352
+ * once — `DoDragDrop` runs its modal loop on the bridge's UI thread, and
353
+ * the gesture reports back as `drag-session-moved` and
354
+ * `drag-session-ended`.
355
+ *
356
+ * And nothing stops here. That is the whole point of the thread split: the
357
+ * frame clock keeps ticking, React keeps committing and a `<popup
358
+ * dragPreview>` mounted by `onDragStart` is painted while the shell owns
359
+ * the pointer — where the cocoa backend's pump does not return until the
360
+ * drop.
361
+ */
362
+ beginDrag(session) {
363
+ if (this.destroyed) return null;
364
+ return this._native.beginDrag(this.id, dragSpec(session));
365
+ }
366
+
367
+ setCursor(name) {
368
+ this.cursor = name;
369
+ this._native.setCursor?.(this.id, name);
370
+ }
371
+
372
+ /**
373
+ * What `<popup grab>` asks for, by the effect rather than the mechanism.
374
+ *
375
+ * There is no cross-application pointer grab on Windows: `SetCapture` sends
376
+ * a window the mouse only while a button is already down, so a *press* that
377
+ * starts over another window never arrives here. What the grab is actually
378
+ * for is one thing — "tell me when the user pressed somewhere else, so the
379
+ * menu can close" — and that is observable without it:
380
+ *
381
+ * - a press delivered to **another window of this application**, which is
382
+ * a click in the owner behind the menu, or in a second window, or in a
383
+ * menu this one opened from;
384
+ * - this application **losing activation**, which is a press in another
385
+ * application or on the desktop. A popup is `WS_EX_NOACTIVATE`, so it
386
+ * never takes activation itself and opening one raises no blur —
387
+ * measured, because the whole rule rests on it.
388
+ *
389
+ * The app watches both and answers with the press the tree expects
390
+ * (src/win32/app.js `_dismissOutsidePopups`), the way the Wayland backend
391
+ * answers `xdg_popup.popup_done` with one (wayland/backendwindow.js).
392
+ *
393
+ * **What it does not catch**: a press on the *non-client* area of one of our
394
+ * own windows — a title bar, a resize border. The bridge does not report
395
+ * those, so a menu left open while the user drags the window behind it stays
396
+ * open. X11's grab covers that case and this does not; it is the one gap,
397
+ * and it is narrower than the one it replaces.
398
+ *
399
+ * The callback reports success because the behaviour it stands for is here
400
+ * now. It used to say the same thing while nothing was watching, which is
401
+ * the worst of both: a caller that checked was told a grab it did not have
402
+ * had been taken.
403
+ */
404
+ grabPointer(options, cb) {
405
+ this.app._dismissOnOutside?.add(this);
406
+ cb?.(null, 0);
407
+ }
408
+
409
+ ungrabPointer() {
410
+ this.app._dismissOnOutside?.delete(this);
411
+ }
412
+
413
+ /**
414
+ * A press that landed outside this window, as the tree hears it.
415
+ *
416
+ * Negative coordinates are what make it a dismissal rather than a click:
417
+ * `_pressOutside` in src/events.js compares against the window's own bounds,
418
+ * and answers anything outside them with `onDismiss`. The same made-up press
419
+ * the Wayland backend sends for `popup_done`, for the same reason — the
420
+ * platform kept the real one.
421
+ */
422
+ _dismissFromOutside() {
423
+ if (this.destroyed) return;
424
+ this.emit('mousedown', {
425
+ x: -1,
426
+ y: -1,
427
+ rootx: -1,
428
+ rooty: -1,
429
+ keycode: 1,
430
+ buttons: 0,
431
+ dismissed: true,
432
+ });
433
+ }
434
+
435
+ // The keyboard's half has no Windows mechanism at all and nothing in the
436
+ // tree reads its result, so it stays honest about doing nothing: a popup
437
+ // that asked for keys gets them only while it is the foreground window.
438
+ grabKeyboard(options, cb) {
439
+ cb?.(null, 0);
440
+ }
441
+ ungrabKeyboard() {}
442
+
443
+ selectXI2() {
444
+ return Promise.resolve(false);
445
+ }
446
+
447
+ // --- painting ------------------------------------------------------------
448
+
449
+ getContext() {
450
+ if (this._ctx) return this._ctx;
451
+ this._ctx = new BackendContext2D(
452
+ this._native,
453
+ () => this._surface,
454
+ () => this._gen,
455
+ );
456
+ // Reading a *window* is not reading a surface. The surface handle the
457
+ // context draws through belongs to one BeginDraw and is write-only
458
+ // anyway; what a caller asking a window for its pixels means is "what is
459
+ // on screen", and only DWM has that (`windowPixels`, src/win32.cc).
460
+ //
461
+ // The callback form is the contract — the X backend's read is a round
462
+ // trip and every caller is written for one (examples/configurator reads
463
+ // its own UI this way to put it on the laptop's screen). Here the answer
464
+ // is already in hand, so it is delivered on a microtask rather than
465
+ // pretended to be slower than it is.
466
+ this._ctx.getImageData = (x, y, width, height, cb) => {
467
+ const read = () => {
468
+ const w = Math.round(width);
469
+ const h = Math.round(height);
470
+ const bytes = this._native.windowPixels(
471
+ this.id,
472
+ Math.round(x),
473
+ Math.round(y),
474
+ w,
475
+ h,
476
+ );
477
+ if (!bytes) throw new Error('react-x11: the window could not be read');
478
+ if (DEBUG) {
479
+ let lit = 0;
480
+ for (let i = 0; i < bytes.length; i += 4) {
481
+ if (bytes[i] || bytes[i + 1] || bytes[i + 2]) lit++;
482
+ }
483
+ console.error(
484
+ `[win32] getImageData ${x},${y} ${w}x${h} -> ${lit}/${bytes.length / 4} lit`,
485
+ );
486
+ }
487
+ return {
488
+ data: new Uint8ClampedArray(
489
+ bytes.buffer,
490
+ bytes.byteOffset,
491
+ bytes.length,
492
+ ),
493
+ width: w,
494
+ height: h,
495
+ };
496
+ };
497
+ if (typeof cb !== 'function') return read();
498
+ let result;
499
+ let failure;
500
+ try {
501
+ result = read();
502
+ } catch (err) {
503
+ failure = err;
504
+ }
505
+ queueMicrotask(() => cb(failure ?? null, result));
506
+ return undefined;
507
+ };
508
+ return this._ctx;
509
+ }
510
+
511
+ /**
512
+ * The frame. One `BeginDraw` per damage rect, which is the X11 damage model
513
+ * verbatim — every pixel inside the rect is repainted, every pixel outside
514
+ * it is kept by DirectComposition — and one `Commit` for the lot, which is
515
+ * the atomic frame the node model relies on.
516
+ *
517
+ * The generation is bumped per rect because each BeginDraw hands back a
518
+ * *different* Direct2D context with none of the previous one's state: the
519
+ * context wrapper reads the bump and pushes its sticky state back in.
520
+ */
521
+ presentFrame(node, damage) {
522
+ if (DEBUG) {
523
+ console.error(
524
+ `[win32] presentFrame window=${this.id} composed=${this._composed} ` +
525
+ `size=${this.width}x${this.height} rects=${damage ? damage.length : 'full'}` +
526
+ (this._owesFullPaint ? ' (owes full)' : ''),
527
+ );
528
+ }
529
+ if (this.destroyed) return;
530
+ if (!this._composed) {
531
+ // Nothing to paint into yet. The frame is not merely skipped: what it
532
+ // would have covered is remembered, because the damage it carried is
533
+ // gone once this returns and the next frame's bound is whatever has been
534
+ // claimed since — which, on a tree with an animation in it, is a handful
535
+ // of small rects. That is exactly how this window came up transparent
536
+ // with a perfectly healthy-looking frame log.
537
+ this._owesFullPaint = true;
538
+ return;
539
+ }
540
+ const ctx = this.getContext();
541
+ // REACT_X11_WIN32_FULL_REPAINT=1 throws the damage away and paints the
542
+ // whole window every frame. Slow on purpose: it is the control for
543
+ // "is this a damage bug", which no amount of reading the rects settles.
544
+ if (process.env.REACT_X11_WIN32_FULL_REPAINT === '1') damage = null;
545
+ if (this._owesFullPaint) {
546
+ this._owesFullPaint = false;
547
+ damage = null;
548
+ }
549
+ const rects = damage ?? [null];
550
+ let painted = false;
551
+ for (const rect of rects) {
552
+ const r = rect ?? { x: 0, y: 0, width: this.width, height: this.height };
553
+ const w = Math.min(this.width - Math.max(0, r.x), Math.ceil(r.width));
554
+ const h = Math.min(this.height - Math.max(0, r.y), Math.ceil(r.height));
555
+ if (!(w > 0 && h > 0)) continue;
556
+ const handle = this._native.beginDraw(
557
+ this.id,
558
+ Math.max(0, Math.floor(r.x)),
559
+ Math.max(0, Math.floor(r.y)),
560
+ w,
561
+ h,
562
+ );
563
+ if (DEBUG) {
564
+ console.error(
565
+ `[win32] rect ${Math.floor(r.x)},${Math.floor(r.y)} ${w}x${h} ` +
566
+ `-> surface ${handle}`,
567
+ );
568
+ }
569
+ if (!handle) continue;
570
+ this._surface = handle;
571
+ this._gen++;
572
+ try {
573
+ node._paintRegion(ctx, rect, this.width, this.height);
574
+ painted = true;
575
+ } finally {
576
+ this._surface = 0;
577
+ this._native.endDraw(this.id);
578
+ }
579
+ }
580
+ if (painted) this._native.commit();
581
+ }
582
+
583
+ /**
584
+ * The scroll-blit fast path: `IDCompositionSurface::Scroll`, which moves the
585
+ * surviving band inside the surface on the GPU. The exposed strip is
586
+ * repainted by the frame's own damage, exactly as on X11.
587
+ */
588
+ scrollRegion(rect, dx, dy) {
589
+ if (!this._composed || this.destroyed) return false;
590
+ if (DEBUG) {
591
+ console.error(
592
+ `[win32] scrollRegion ${Math.round(rect.x)},${Math.round(rect.y)} ` +
593
+ `${Math.round(rect.width)}x${Math.round(rect.height)} by ${dx},${dy}`,
594
+ );
595
+ }
596
+ return this._native.scrollRegion(
597
+ this.id,
598
+ Math.round(rect.x),
599
+ Math.round(rect.y),
600
+ Math.round(rect.width),
601
+ Math.round(rect.height),
602
+ Math.round(dx),
603
+ Math.round(dy),
604
+ );
605
+ }
606
+
607
+ /** DirectComposition does not hold a frame back the way an X server's fence
608
+ * or a WindowServer's buffer does: Commit is asynchronous and the surface
609
+ * retains its own pixels. So there is never a frame in flight to wait for. */
610
+ frameInFlight() {
611
+ return false;
612
+ }
613
+
614
+ requestAnimationFrame(cb) {
615
+ return this.app._requestFrame(cb, this);
616
+ }
617
+
618
+ present() {
619
+ // Nothing to flip: presentFrame committed. Kept because the frame loop
620
+ // calls it on every window it paced.
621
+ }
622
+
623
+ /** A DirectComposition surface is write-only, so a window's pixels are not
624
+ * readable the way an IOSurface's or an X drawable's are. docs/windows.md
625
+ * answers this with PrintWindow once the commit has completed; not bound
626
+ * yet, and it says so rather than answering with something wrong. */
627
+ /**
628
+ * The window's pixels, as RGBA. PrintWindow is the door DWM opens on a
629
+ * composed window — see `windowPixels` in src/win32.cc for which flags are
630
+ * the ones that work and what the others answer instead.
631
+ */
632
+ snapshot() {
633
+ const width = Math.round(this.width);
634
+ const height = Math.round(this.height);
635
+ const bytes = this._native.windowPixels(this.id, 0, 0, width, height);
636
+ if (!bytes) {
637
+ return Promise.reject(
638
+ new Error(
639
+ 'react-x11: the window could not be read — PrintWindow refused it. ' +
640
+ 'A window that is minimised or not yet composed has nothing to ' +
641
+ 'read; draw into an offscreen surface and read that instead.',
642
+ ),
643
+ );
644
+ }
645
+ return Promise.resolve({
646
+ data: new Uint8ClampedArray(bytes.buffer, bytes.byteOffset, bytes.length),
647
+ width,
648
+ height,
649
+ });
650
+ }
651
+
652
+ /** How many damage rects a frame may carry before it collapses to their
653
+ * box. Each is a BeginDraw with a fixed cost, so the right number is a fact
654
+ * about DirectComposition rather than a choice — 16 is Cocoa's, kept until
655
+ * it is measured here. */
656
+ get damageRectCap() {
657
+ return 16;
658
+ }
659
+ }