react-x11 2.10.2 → 2.12.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 (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +304 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/screencolor.js +62 -0
  20. package/src/cocoa/threaded.js +246 -0
  21. package/src/cocoa/window.js +256 -42
  22. package/src/components/Select.js +2 -2
  23. package/src/components/anchor.js +3 -3
  24. package/src/components/native.js +12 -7
  25. package/src/components/theme.js +2 -2
  26. package/src/debug.js +1 -1
  27. package/src/decorations.js +1 -1
  28. package/src/editmenu.js +2 -2
  29. package/src/errors.js +46 -0
  30. package/src/events.js +6 -6
  31. package/src/foreignnodes.js +3 -2
  32. package/src/frames.js +2 -2
  33. package/src/glnodes.js +1 -1
  34. package/src/grid.js +1653 -0
  35. package/src/host.d.ts +230 -0
  36. package/src/host.js +11 -3
  37. package/src/imagesource.js +1 -1
  38. package/src/index.d.ts +21 -4
  39. package/src/index.js +9 -1
  40. package/src/layouts.js +721 -0
  41. package/src/node.d.ts +4 -2
  42. package/src/node.js +19 -21
  43. package/src/nodes/animation.js +644 -0
  44. package/src/nodes/box.js +21 -0
  45. package/src/nodes/boxpaint.js +473 -0
  46. package/src/nodes/canvas.js +269 -0
  47. package/src/nodes/cascade.js +600 -0
  48. package/src/nodes/damage.js +183 -0
  49. package/src/nodes/edithistory.js +124 -0
  50. package/src/nodes/editmenupopup.js +260 -0
  51. package/src/nodes/hittest.js +185 -0
  52. package/src/nodes/image.js +266 -0
  53. package/src/nodes/install.js +75 -0
  54. package/src/nodes/invalidate.js +465 -0
  55. package/src/nodes/kinds.js +31 -0
  56. package/src/nodes/layout.js +439 -0
  57. package/src/nodes/layouthost.js +949 -0
  58. package/src/nodes/node.js +868 -0
  59. package/src/nodes/paint.js +466 -0
  60. package/src/nodes/position.js +366 -0
  61. package/src/nodes/preedit.js +127 -0
  62. package/src/nodes/queries.js +330 -0
  63. package/src/nodes/rects.js +102 -0
  64. package/src/nodes/scrollable.js +891 -0
  65. package/src/nodes/scrollbars.js +138 -0
  66. package/src/nodes/scrollblit.js +1034 -0
  67. package/src/nodes/selectable.js +142 -0
  68. package/src/nodes/styling.js +225 -0
  69. package/src/nodes/text.js +649 -0
  70. package/src/nodes/textarea.js +391 -0
  71. package/src/nodes/textinput.js +1146 -0
  72. package/src/nodes/util.js +17 -0
  73. package/src/nodes/window/anchoring.js +161 -0
  74. package/src/nodes/window/capabilities.js +190 -0
  75. package/src/nodes/window/debugpaint.js +83 -0
  76. package/src/nodes/window/droptarget.js +145 -0
  77. package/src/nodes/window/floors.js +577 -0
  78. package/src/nodes/window/flush.js +334 -0
  79. package/src/nodes/window/hints.js +482 -0
  80. package/src/nodes/window/listeners.js +222 -0
  81. package/src/nodes/window/popup.js +71 -0
  82. package/src/nodes/window/size.js +591 -0
  83. package/src/nodes/window/window.js +945 -0
  84. package/src/palette.js +1 -1
  85. package/src/registry.js +7 -3
  86. package/src/screencolor.js +212 -38
  87. package/src/screencolorhooks.js +6 -2
  88. package/src/styles.js +137 -15
  89. package/src/svgnodes.js +2 -1
  90. package/src/testing/harness.js +2 -2
  91. package/src/textselection.js +5 -3
  92. package/src/trace-registry.js +1 -1
  93. package/src/types/components.d.ts +38 -6
  94. package/src/types/elements.d.ts +11 -1
  95. package/src/types/nodes.d.ts +33 -5
  96. package/src/types/screencolor.d.ts +20 -14
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
@@ -0,0 +1,945 @@
1
+ // <window>: the node that owns a real X11 window. Realizing it, mapping it at
2
+ // the end of the commit that created it (#201), stacking its child windows,
3
+ // and the props and tree operations a window handles differently from a
4
+ // box. The rest of WindowNode lives with its concerns — the window-only ones
5
+ // beside this file, and its halves of the ones it shares with Node beside
6
+ // Node's — installed at the bottom (see install.js).
7
+
8
+ import { applyLayoutStyle, paintPropsChanged } from '../../styles.js';
9
+ import { EventManager } from '../../events.js';
10
+ import { forgetTopLevel, hasDropProps } from '../../dnd.js';
11
+ import { clearPendingFrame } from '../../frames.js';
12
+ import { FramePacer } from '../../pacing.js';
13
+ import { endWindowState } from '../../windowstate.js';
14
+ import { anchorOffscreen } from '../../anchor.js';
15
+ import { topLevelWindows } from '../../windowid.js';
16
+ import { WindowAnimation } from '../animation.js';
17
+ import { WindowCascade } from '../cascade.js';
18
+ import { NO_DAMAGE } from '../damage.js';
19
+ import { installMethods } from '../install.js';
20
+ import { WindowInvalidate } from '../invalidate.js';
21
+ import { WindowLayoutHost } from '../layouthost.js';
22
+ import { DEVTOOLS_FAKE_DOCUMENT, Node } from '../node.js';
23
+ import { WindowPaint } from '../paint.js';
24
+ import { WindowPosition } from '../position.js';
25
+ import { WindowQueries } from '../queries.js';
26
+ import { Scrollable } from '../scrollable.js';
27
+ import { WindowScrollBlit } from '../scrollblit.js';
28
+ import { WindowAnchoring } from './anchoring.js';
29
+ import { pixelFor, WindowCapabilities } from './capabilities.js';
30
+ import { WindowDebugPaint } from './debugpaint.js';
31
+ import { WindowDropTarget } from './droptarget.js';
32
+ import { WindowFlush } from './flush.js';
33
+ import {
34
+ isAutoSize,
35
+ canonicalSize,
36
+ assertWindowSize,
37
+ scaleWindowGeometry,
38
+ windowAttributes,
39
+ windowStates,
40
+ applyWindowStates,
41
+ applyDecorations,
42
+ WINDOW_SEMANTIC_NAMES,
43
+ WindowHints,
44
+ } from './hints.js';
45
+ import { WindowListeners } from './listeners.js';
46
+ import { WindowSize } from './size.js';
47
+
48
+ // X ConfigureWindow stack-mode: Below places the window directly under the
49
+ // named sibling (X11 protocol, ConfigureWindow).
50
+ const STACK_BELOW = 1;
51
+
52
+ // Windows whose child stacking order may have gone stale during the commit
53
+ // in progress; drained by flushWindowRestacks from resetAfterCommit.
54
+ const pendingRestack = new Set();
55
+
56
+ // Windows realized during the commit in progress, waiting to be mapped;
57
+ // drained by flushWindowMaps from resetAfterCommit. See beginWindowMaps.
58
+ const pendingMaps = new Set();
59
+ let inCommit = false;
60
+
61
+ /**
62
+ * A window maps at the *end* of the commit that realized it, not when
63
+ * `realize()` runs.
64
+ *
65
+ * React inserts a host instance before it hides it: `hideInstance` runs
66
+ * after the whole mutation phase, so a `<window>` born inside a hidden
67
+ * `<Activity>` — or inside a `<Suspense>` that suspends on its first render
68
+ * — used to be mapped and unmapped back to back. That pair is only safe
69
+ * when nothing redirects the map. Under a window manager holding
70
+ * SubstructureRedirect on the root the MapWindow is **not performed**: the
71
+ * server turns it into a MapRequest and leaves the window unmapped, so the
72
+ * UnmapWindow that follows lands on an already-unmapped window and is
73
+ * discarded. The window manager then services its MapRequest and the
74
+ * "hidden" window is on screen for good (issue #201).
75
+ *
76
+ * Deferring costs nothing — `resetAfterCommit` runs inside the same
77
+ * synchronous `render()` — and it means the map is decided at the one
78
+ * moment when whether the window is hidden is already known.
79
+ *
80
+ * Outside a commit (a `<popup>` realized from `commitMount`, which runs in
81
+ * the layout phase, or one built imperatively like the text controls' edit
82
+ * menu) there is no such phase to wait for, and no hiding on the way
83
+ * either: those map immediately.
84
+ */
85
+ export function beginWindowMaps() {
86
+ // A commit that never reached `resetAfterCommit` left its queue behind,
87
+ // and a window that is owed a map had better get one late rather than
88
+ // never — that failure mode is an application with no windows in it.
89
+ flushWindowMaps();
90
+ inCommit = true;
91
+ }
92
+
93
+ /** Map every window this commit realized and did not then hide. */
94
+ export function flushWindowMaps() {
95
+ inCommit = false;
96
+ const nodes = [...pendingMaps];
97
+ pendingMaps.clear();
98
+ for (const node of nodes) node._mapNow();
99
+ }
100
+
101
+ // The server-side event mask every realized window ends up with. The
102
+ // subscriptions are a constant — the EventManager's pointer/key/focus
103
+ // listeners, the window's own resize/draw/expose pair, the backing store's
104
+ // Exposure — but ntk grows the mask lazily, one ChangeWindowAttributes per
105
+ // first listener of each kind: nine requests per window for a value known
106
+ // before the window exists. Declaring the union in CreateWindow makes every
107
+ // one of those a detected no-op (ntk ORs `eventMask` into what it derives,
108
+ // and `newListener` only issues the request for bits still missing).
109
+ //
110
+ // The values are core-protocol SETofEVENT bits, fixed since X11R1 — the
111
+ // same numbers ntk's own table maps event names to, written out because ntk
112
+ // does not export them. EnterWindow is deliberately absent: hover tracking
113
+ // reads `mousemove`/`mouseout` only, and parity with the lazily-grown mask
114
+ // is what keeps this a request-count change and nothing else.
115
+ const WINDOW_EVENT_MASK =
116
+ (1 << 0) | // KeyPress — keydown
117
+ (1 << 1) | // KeyRelease — keyup
118
+ (1 << 2) | // ButtonPress — mousedown, and the core half of wheel
119
+ (1 << 3) | // ButtonRelease — mouseup
120
+ (1 << 5) | // LeaveWindow — mouseout
121
+ (1 << 6) | // PointerMotion — mousemove (hover, drag)
122
+ (1 << 15) | // Exposure — draw/expose, and the backing store's redraws
123
+ (1 << 17) | // StructureNotify — resize/map/destroy (ntk's own baseline)
124
+ (1 << 21); // FocusChange — focus/blur
125
+
126
+ /** Apply any child-window stacking changes the commit produced, once. */
127
+ export function flushWindowRestacks() {
128
+ const nodes = [...pendingRestack];
129
+ pendingRestack.clear();
130
+ for (const node of nodes) node._restackWindowChildren();
131
+ }
132
+
133
+ /**
134
+ * <window>: backed by a real X11 window. Acts as the flex root and
135
+ * paint/event root for its drawn subtree. The node is a lightweight handle
136
+ * during the render phase — the real window is created top-down in the
137
+ * commit phase by realize(), so every CreateWindow names its actual parent
138
+ * from the start (no ReparentWindow, no override-redirect staging;
139
+ * issue #4).
140
+ */
141
+ export class WindowNode extends Scrollable(Node) {
142
+ constructor(app, attributes, props) {
143
+ super('window', props, app, { yoga: true });
144
+ assertWindowSize(props, this.kind);
145
+ this.root = this;
146
+ this.attributes = attributes;
147
+ this.window = null;
148
+ // `hidden` has two writers — the reconciler (React hiding a subtree for
149
+ // `<Suspense>`/`<Activity>`) and the element's own `hidden` prop — and
150
+ // the window is off screen while *either* says so. The reconciler's half
151
+ // is remembered here so that a `<Suspense>` revealing its content does
152
+ // not map a window whose prop still hides it. `this.hidden` stays the
153
+ // one flag everything reads (`_mapNow`, painting, a11y, anchoring).
154
+ this._reactHidden = false;
155
+ this.hidden = Boolean(props.hidden);
156
+ // whether this is the tree's own top-level window rather than a nested
157
+ // one or a popup — decided by realize(), read when it maps
158
+ this._topLevel = false;
159
+ // set by realize() only once the ARGB visual is actually there, so the
160
+ // paint path never assumes an alpha channel the window does not have
161
+ this._transparent = false;
162
+ // What `@supports` blocks are answered from, and what the paint path
163
+ // reads. `transparency` needs *both* halves — an alpha channel to write
164
+ // and a compositor to blend it — and starts false so a window that has
165
+ // not resolved either yet paints the design that works everywhere.
166
+ this._capabilities = { transparency: false };
167
+ this._unwatchCompositing = null;
168
+ this.needsLayout = true;
169
+ this.needsPaint = true;
170
+ this._scheduled = false;
171
+ // The frame pacer (src/pacing.js): whether a claim waits before its
172
+ // frame is scheduled, priced by what the last frames cost. Off unless
173
+ // the `frameRate` prop, the root's default or the environment says
174
+ // otherwise — resolved again whenever the prop changes.
175
+ this._pacer = new FramePacer();
176
+ this._framePolicy = null;
177
+ this._syncFramePolicy();
178
+ // a claim raised by the frame on itself is scheduled once the frame is
179
+ // over and its cost is known (`flush`)
180
+ this._inFlush = false;
181
+ this._claimAfterFlush = false;
182
+ // the nodes answering `opaqueRect()`, and — during a paint pass one of
183
+ // them covers — that node with its ancestors, whose fills are skipped
184
+ // (`_coverFor`, `Node._paintBackground`)
185
+ this._opaqueNodes = new Set();
186
+ this._coverChain = null;
187
+ // Nodes that want the `attention` event (ntk#37) — an
188
+ // `unstable_onAttention` prop,
189
+ // an `:attention` block, or both. Built before the EventManager so the
190
+ // manager can hold the reference itself: the whole feature has to be
191
+ // behind one `size` read on the motion path, and a tree that never asked
192
+ // for attention must not pay a property walk to find that out.
193
+ this._attentionNodes = new Set();
194
+ this.events = new EventManager(this);
195
+ // ids of the child windows in the order the *server* stacks them,
196
+ // bottom to top — see _restackWindowChildren
197
+ this._xStack = [];
198
+ // nodes with a transition in flight
199
+ this._animating = new Set();
200
+ // …and the nodes whose style declares a *loop*, running or not: the set
201
+ // every stop condition is applied over, and what decides whether this
202
+ // window is watching its own visibility at all
203
+ this._loopNodes = new Set();
204
+ this._loopsPaused = false;
205
+ this._loopWatch = null;
206
+ // nodes with `@width`/`@height` blocks, and the size they last matched
207
+ // against
208
+ this._sizeQueryNodes = new Set();
209
+ // nodes with `@supports` blocks, re-resolved when the server's answer
210
+ // changes rather than on every layout
211
+ this._supportsQueryNodes = new Set();
212
+ // nodes with `@container` blocks, re-resolved after every layout pass
213
+ // against the containers they ask about — see _resolveContainerQueries.
214
+ // `_cqFresh` is true while a pass this window just ran is being settled,
215
+ // when every attached node has a computed size to offer
216
+ this._containerQueryNodes = new Set();
217
+ this._cqFresh = false;
218
+ // nodes whose `position` is placed after layout (sticky among them),
219
+ // placed after every layout pass — see _placeNodes — and whether one
220
+ // asked for a frame of its own, with nothing to lay out (an animated
221
+ // position)
222
+ this._placedNodes = new Set();
223
+ this._placementsDue = false;
224
+ // nodes arranging their children with a layout algorithm, placed at the
225
+ // end of every layout step — see _placeLayoutHosts — and the ones whose
226
+ // algorithm threw this pass, laid out as flexbox before the frame is done
227
+ this._layoutHosts = new Set();
228
+ this._failedHosts = new Set();
229
+ // nodes whose child list changed and whose own size is pinned: their new
230
+ // arrangement is only measurable once layout has run (see
231
+ // Node._childListChanged)
232
+ this._reflowed = new Set();
233
+ this.querySize = null;
234
+ // The geometry we last asked the server for, and whether anything else
235
+ // has since decided otherwise. Together they are the rule for `'auto'`:
236
+ // it keeps up with the content until someone takes the size over, and
237
+ // the only thing that ever does is the user dragging an edge.
238
+ this._requestedSize = null;
239
+ this._userSized = false;
240
+ // the last `WM_NORMAL_HINTS` struct written and the size it was written
241
+ // at, so that a bound measured every frame is only *sent* on the frames
242
+ // it moves (see _sendSizeHints for why the size is part of it)
243
+ this._sentHints = null;
244
+ this._sentHintsAt = null;
245
+ // The automatic minimum size (#249): whether the floors are still the
246
+ // answer, the width the height half of them was measured for, the nodes
247
+ // this frame found stale (`collectFloorStale`), and two counters the
248
+ // tests read — layout passes over the root and nodes measured.
249
+ this._floorsDirty = true;
250
+ this._floorsWidth = null;
251
+ this._floorsStale = new Set();
252
+ // whether the first floors pass has run (`collectFloorStale`'s sweep)
253
+ this._floorsSwept = false;
254
+ this._floorsMeasured = 0;
255
+ this._layoutPasses = 0;
256
+ // whether the tree's CONTENT moved since the floors were measured — a
257
+ // resize alone does not, which is what lets a live resize defer them
258
+ this._floorsContentDirty = true;
259
+ this._floorsCatchUp = false;
260
+ }
261
+
262
+ /** Create the real X11 window (commit phase only). Children windows are
263
+ * realized against this window, then mapped before it so the whole
264
+ * subtree appears at once when the outermost window maps. */
265
+ realize(parentWindow) {
266
+ if (this.window || this.destroyed) return;
267
+ const attributes = { ...this.attributes };
268
+ if (parentWindow) {
269
+ attributes.parent = parentWindow;
270
+ }
271
+ // Before CreateWindow, so an auto-sized window is *born* the right size.
272
+ // Doing it after would mean a window mapped at 800x800 and corrected a
273
+ // frame later, which is the jump this exists to avoid.
274
+ const natural = this._measure();
275
+ attributes.width = natural.width;
276
+ attributes.height = natural.height;
277
+ this._requestedSize = { width: natural.width, height: natural.height };
278
+ // And straight after it, for the same reason: the placement is a
279
+ // function of the size, so this is the first moment it can be worked
280
+ // out — and the last one before the window exists at a position.
281
+ const placed = this._anchorPlacement(natural);
282
+ if (placed) {
283
+ attributes.x = placed.x;
284
+ attributes.y = placed.y;
285
+ this._placedAt = { x: placed.x, y: placed.y };
286
+ }
287
+ // A bound the content decides is a number by now, and the window manager
288
+ // reads `WM_NORMAL_HINTS` when it frames the window — so it goes in with
289
+ // the creation attributes rather than chasing the map with a second
290
+ // property write.
291
+ if (Object.keys(natural.hints).length > 0) {
292
+ this._sentHints = this._hintsToSend(this.props, natural.hints);
293
+ attributes.sizeHints = this._sentHints;
294
+ }
295
+ // **What the server paints into newly exposed area.** A resize enlarges
296
+ // the window before the app can possibly have drawn the new part, and X
297
+ // fills it with this attribute in the meantime — so without one, growing
298
+ // a window flashes whatever the server's default is, which on a dark
299
+ // palette is a bright rectangle. Setting it to the colour that is about
300
+ // to be painted there makes the flash the same colour as the result.
301
+ const pixel = pixelFor(this._windowBackground());
302
+ if (pixel !== null) {
303
+ attributes.backgroundPixel = pixel;
304
+ this._backgroundPixel = pixel;
305
+ }
306
+ // Before the window exists, because a visual is a CreateWindow field: a
307
+ // window cannot become transparent later, which is also why `transparent`
308
+ // is read here and never in the update path. It overrides the pixel above
309
+ // with 0 — transparent black — when the ARGB visual is really there.
310
+ if (this.props.transparent)
311
+ Object.assign(attributes, this._argbAttributes());
312
+ // **Smooth scrolling, where the server can — and where the window turns
313
+ // out to want it.** XI2 carries a scroll as the device's own valuators,
314
+ // so a touchpad's two-finger scroll arrives as the fractions of a notch
315
+ // it was rather than as the whole clicks of button 4/5 the server
316
+ // emulates for clients that cannot read them. ntk translates the device
317
+ // events back into the core-shaped ones the rest of this file reads, and
318
+ // falls back to those buttons where there is no XI2 (issue #273).
319
+ //
320
+ // **It is not free, which is why it is no longer selected up front.** An
321
+ // XI2 selection *replaces* the core one for the same event type, and an
322
+ // XIMotion is 136 bytes on the wire against a core MotionNotify's 32
323
+ // (`npm run xi2:probe`, Xorg 21.1). Motion is the one event that keeps
324
+ // arriving at frame rate for as long as the pointer is over the window,
325
+ // so an eager selection bills every window ~8 KB/s of pointer traffic
326
+ // while the pointer crosses it — for a feature most windows never use. A
327
+ // dialog, a toolbar, a form, a splash screen never see a wheel at all.
328
+ //
329
+ // So `'auto'` — the default — creates the window on core events and takes
330
+ // the selection the first time the window is actually scrolled
331
+ // (`upgradeToXI2`, from `EventManager._onWheel`). What that costs is the
332
+ // opening event of the first gesture in a window's life, and it costs
333
+ // less than it sounds: a mouse wheel reports whole notches whichever way
334
+ // the scroll arrived, and ntk's `ScrollTracker` treats the first valuator
335
+ // event as a seed with no distance to report — so under an eager
336
+ // selection that same first event moves nothing at all. `xi2` selects at
337
+ // creation for an app whose whole interaction is the touchpad; `false`
338
+ // refuses the selection outright.
339
+ //
340
+ // Never on a `<popup>`, which is the window that holds a pointer grab: a
341
+ // core grab delivers core events, and ntk drops the emulated wheel
342
+ // buttons on a window whose valuators are flowing — a menu that had
343
+ // selected XI2 would be a menu the wheel could not reach while it was
344
+ // grabbing. An explicit `xi2` still wins there, because an app that says
345
+ // so has said so.
346
+ const wantsXI2 = this.props.xi2 ?? 'auto';
347
+ // `'auto'` is ours and must not reach ntk, whose `args.xi2` is truthiness
348
+ attributes.xi2 = wantsXI2 === true;
349
+ this._xi2Pending = wantsXI2 === 'auto' && !this.isPopup;
350
+ // The full event mask, declared at creation — see WINDOW_EVENT_MASK.
351
+ attributes.eventMask = (attributes.eventMask ?? 0) | WINDOW_EVENT_MASK;
352
+ const wnd = this.app.createWindow(attributes);
353
+ this.window = wnd;
354
+ // Now that the visual is known: settle the capabilities, re-resolve any
355
+ // `@supports` block against them, and start following the compositor.
356
+ // Before the first paint, and before children realize against it.
357
+ this._watchCapabilities();
358
+ wnd._reactX11Node = this;
359
+ wnd._reactFiber = this._reactFiber;
360
+ // windows are DevTools public instances too — see Node.getClientRects
361
+ const s = this.scale;
362
+ wnd.getClientRects ??= () => [
363
+ {
364
+ x: 0,
365
+ y: 0,
366
+ left: 0,
367
+ top: 0,
368
+ width: wnd.width / s,
369
+ height: wnd.height / s,
370
+ },
371
+ ];
372
+ wnd.measure ??= (callback) =>
373
+ callback?.(0, 0, wnd.width / s, wnd.height / s, 0, 0);
374
+ wnd.ownerDocument ??= DEVTOOLS_FAKE_DOCUMENT;
375
+ this._attachWindowListeners(parentWindow);
376
+ for (const child of this.children) {
377
+ if (child.isWindow && !child.isPopup) {
378
+ child.realize(wnd);
379
+ if (child.window) this._xStack.push(child.window.id);
380
+ }
381
+ }
382
+ this._restackWindowChildren();
383
+ // <glarea>s and <foreign>s mounted before the window existed own a
384
+ // child X window too
385
+ this._realizeChildWindows(this);
386
+ // Before the map, deliberately. EWMH 7.7 gives an unmapped window a
387
+ // different mechanism — it *declares* its initial state by writing the
388
+ // property, where a mapped one has to *ask* the window manager — and
389
+ // declaring is the only way to open already fullscreen rather than
390
+ // flashing at the normal size first. Same for the Motif hint: a WM
391
+ // reads decorations when it frames the window, which is at map time.
392
+ if (this.props.decorations === false) applyDecorations(wnd, false);
393
+ applyWindowStates(wnd, [...windowStates(this.props)], 'add');
394
+ // ICCCM 4.1.2.6 has the window manager read WM_TRANSIENT_FOR when the
395
+ // transient is mapped, so this belongs before the map too. ntk writes it
396
+ // with predefined atoms and no round trip, so "before" is free.
397
+ this._applyTransientFor(this.props.transientFor);
398
+ // Top-level windows advertise XDND before the map, like the EWMH
399
+ // properties above: a declaration, made before anyone can look. Child
400
+ // <window>s never advertise (XDND v3 puts XdndAware on top-levels
401
+ // only); drags over them arrive here and are routed down in JS.
402
+ if (!parentWindow) this._initDnd();
403
+ // The launch's own properties, and the same "before the map" rule as
404
+ // everything above it: EWMH's guarantee about `_NET_WM_USER_TIME` is
405
+ // about the window's state at the moment it is mapped. First toplevel
406
+ // only — a later `<window>` is not the launch (src/startup.js).
407
+ this._topLevel = !parentWindow && !this.isPopup;
408
+ if (this._topLevel) this.app._reactX11Startup?.decorate(wnd);
409
+ // Before the map for the same reason the properties above are: a popup
410
+ // whose anchor is already out of view — an editor scrolled between the
411
+ // keystroke that opened the completion list and the commit that
412
+ // realized it — should never be on screen at all, rather than appear
413
+ // and vanish.
414
+ if (this.props.anchor) {
415
+ this._watchAnchor();
416
+ const node = this._anchorTarget(this.props.anchor.to);
417
+ this._anchorLost = !node || anchorOffscreen(node, this.props.anchor.at);
418
+ }
419
+ // Queued rather than mapped, when there is a commit to queue behind:
420
+ // React hides a subtree only once it has inserted it (beginWindowMaps).
421
+ if (inCommit) pendingMaps.add(this);
422
+ else this._mapNow();
423
+ // ask before anything can be anchored to it, so the first popup is
424
+ // placed as well as the second
425
+ this._refreshScreenOrigin();
426
+ this.invalidate(true, null, 'mount');
427
+ }
428
+
429
+ /**
430
+ * Put the window on screen, unless this commit went on to hide it.
431
+ *
432
+ * The only caller that maps a window for the first time is
433
+ * `flushWindowMaps` (or `realize` itself outside a commit); `setHidden`
434
+ * comes back through here so that a window born hidden and revealed later
435
+ * still ends the startup sequence on its real first map.
436
+ */
437
+ _mapNow() {
438
+ if (this.destroyed || !this.window || this.hidden) return false;
439
+ // An `embeddable` window never maps itself: a window waiting to be
440
+ // embedded is unmapped — that is what waiting looks like — and from the
441
+ // reparent on, mapping is the embedder's decision (ntk's XEmbedSocket
442
+ // maps a plain client the moment it takes it). Self-mapping here would
443
+ // put a frame pane on the desktop as a top-level for the beat before
444
+ // its <Frame> embeds it, long enough for a window manager to frame it.
445
+ if (this.props.embeddable) return false;
446
+ // An anchor that is not on screen is a popup that has nowhere to be
447
+ // (`_followAnchor`); it maps from there, when the anchor comes back.
448
+ if (this._anchorLost) return false;
449
+ this.window.map?.();
450
+ if (this._topLevel) this.app._reactX11Startup?.mapped(this.window);
451
+ // whether the map went out, so `PopupNode` can hang its grab off it
452
+ return true;
453
+ }
454
+
455
+ /**
456
+ * Take the XI2 selection this window was deliberately created without —
457
+ * see `realize()` for why `xi2: 'auto'` starts on core events. Called by
458
+ * `EventManager._onWheel`, so the window that is scrolled is the window
459
+ * that pays for smooth scrolling.
460
+ *
461
+ * **One-shot and one-way.** `_xi2Pending` is cleared before the request
462
+ * goes out, so a burst of wheel events in one frame asks once. Coming back
463
+ * down is not offered: the only signal that would justify it is "nothing in
464
+ * here scrolls any more", which cannot be read without a per-node registry,
465
+ * and getting it wrong drops a live gesture from valuators back to notches
466
+ * mid-scroll — a visible regression, to save bytes on a window the user is
467
+ * actively using.
468
+ *
469
+ * Silent where the server has no XInput2: `selectXI2()` resolves `false`
470
+ * and the window keeps the emulated wheel buttons it already had, which is
471
+ * exactly where an eager selection would have landed too. Feature-detected
472
+ * on the method, for an ntk older than 7.5.0.
473
+ */
474
+ upgradeToXI2() {
475
+ if (!this._xi2Pending || this.destroyed) return;
476
+ this._xi2Pending = false;
477
+ if (typeof this.window?.selectXI2 !== 'function') return;
478
+ // fire and forget, like the eager selection in ntk's own createWindow:
479
+ // until the extension answers the window is on core events, which is
480
+ // where it would have been anyway
481
+ this.window.selectXI2().catch((err) => {
482
+ this.app?.options?.onXError?.(err);
483
+ });
484
+ }
485
+
486
+ /**
487
+ * Where this window's top-left corner actually is on the screen, cached
488
+ * on the ntk window for `anchorRect` to read.
489
+ *
490
+ * It cannot be taken from `window.x`/`y`. Those come from ConfigureNotify,
491
+ * and once a reparenting window manager has put the window inside its
492
+ * frame — which is every WM worth the name — those coordinates are
493
+ * relative to the *frame*, not the root. A popup anchored with them lands
494
+ * near the corner of the screen instead of under its trigger. The server
495
+ * will translate for us, and its answer is right whatever the WM did.
496
+ */
497
+ _refreshScreenOrigin() {
498
+ const wnd = this.window;
499
+ const X = this.app?.X;
500
+ const root = X?.display?.screen?.[0]?.root;
501
+ if (!wnd || root == null || typeof X.TranslateCoordinates !== 'function') {
502
+ return;
503
+ }
504
+ X.TranslateCoordinates(wnd.id, root, 0, 0, (err, res) => {
505
+ if (err || this.destroyed || !this.window) return;
506
+ this.window._screenOrigin = { x: res.destX, y: res.destY };
507
+ this._notifyAnchorChange();
508
+ });
509
+ }
510
+
511
+ /**
512
+ * Subscribe to this window gaining or losing the **window manager's**
513
+ * focus. Returns an unsubscribe function.
514
+ *
515
+ * Deliberately not the same thing as a node's `onBlur`: a window losing
516
+ * focus does not blur the node inside it — the node keeps focus and stops
517
+ * looking active, which is what the DOM does with `document.activeElement`
518
+ * and what a caret coming back where you left it depends on. So nothing in
519
+ * the tree hears about it, and the things that must — a menu holding a
520
+ * pointer grab, most of all — have nowhere else to ask.
521
+ */
522
+ onWindowFocusChange(cb) {
523
+ (this._windowFocusListeners ??= new Set()).add(cb);
524
+ return () => this._windowFocusListeners?.delete(cb);
525
+ }
526
+
527
+ _notifyWindowFocus(focused) {
528
+ if (!this._windowFocusListeners?.size) return;
529
+ for (const cb of [...this._windowFocusListeners]) cb(focused);
530
+ }
531
+
532
+ /**
533
+ * Walk the drawn subtree and give every element that owns a real child X
534
+ * window one — `<glarea>` and `<foreign>`.
535
+ *
536
+ * Here rather than in `createInstance` because the render phase is
537
+ * discardable: a CreateWindow from a render React throws away leaks a
538
+ * server resource, and a ReparentWindow from one has moved another
539
+ * client's window for real (docs/extending.md).
540
+ */
541
+ _realizeChildWindows(node) {
542
+ for (const child of node.children) {
543
+ if (child.isWindow) continue;
544
+ if (child.isGlArea || child.isForeign) child.realize();
545
+ else this._realizeChildWindows(child);
546
+ }
547
+ }
548
+
549
+ get semanticNames() {
550
+ return WINDOW_SEMANTIC_NAMES;
551
+ }
552
+
553
+ /**
554
+ * Whether this is the window whose close button means "quit".
555
+ *
556
+ * Inferred, not declared, because the tree already says it. A window that
557
+ * is somebody's `transientFor` is a dialog *of* that window, and one with
558
+ * an EWMH type of its own (`dialog`, `utility`, `splash`, …) has already
559
+ * announced it is not the main window; what is left, in creation order, is
560
+ * the app. That is the same rule startup.js uses to decide which window
561
+ * carries the launch id, and for one-window apps — nearly all of them — it
562
+ * is not a heuristic at all.
563
+ *
564
+ * A lone window is the app whatever it calls itself: an app whose only
565
+ * window is a `utility` still has to be closable. An app that disagrees
566
+ * with any of this passes `onCloseRequest`, which never reaches here.
567
+ */
568
+ _isPrimaryWindow() {
569
+ const tops = topLevelWindows(this.app);
570
+ if (!tops.includes(this)) return false;
571
+ const candidates = tops.filter((node) => node._isPrimaryCandidate());
572
+ if (candidates.length === 0) return tops.length === 1;
573
+ return candidates[0] === this;
574
+ }
575
+
576
+ /** Top-level, nobody's dialog, and of no special type: a main-window shape. */
577
+ _isPrimaryCandidate() {
578
+ if (this.props.transientFor != null) return false;
579
+ const type = this.props.windowType;
580
+ const plain = (t) => t == null || t === 'normal';
581
+ return Array.isArray(type) ? plain(type[0]) : plain(type);
582
+ }
583
+
584
+ /** Child <window>s in the order they should stack, bottom to top: the same
585
+ * rule drawn children paint by (later sibling on top, `zIndex` first). */
586
+ _windowStackOrder() {
587
+ return this.children
588
+ .filter((c) => c.isWindow && !c.isPopup && c.window)
589
+ .map((node, i) => ({ node, i }))
590
+ .sort(
591
+ (a, b) =>
592
+ (a.node.style.zIndex ?? 0) - (b.node.style.zIndex ?? 0) || a.i - b.i,
593
+ )
594
+ .map((e) => e.node);
595
+ }
596
+
597
+ /**
598
+ * Make the server's stacking order match the JSX order. X stacks a new
599
+ * window on top of its siblings, so plain mount order already comes out
600
+ * right and this sends nothing; it costs requests only when React moves a
601
+ * child window or a `zIndex` changes. Walking top-down and putting each
602
+ * window directly below the one above it fixes any permutation in one
603
+ * pass — after step i, everything from i upwards is a contiguous run in
604
+ * the right order. Top-level windows are excluded on purpose: they are
605
+ * the window manager's to stack, and it redirects the request anyway;
606
+ * so are popups, which are children of the screen root wherever they sit
607
+ * in the tree. Only `<window>` children are ordered against each other —
608
+ * a `<glarea>`'s X window is a sibling at the server, but it belongs to
609
+ * the drawn tree, which has no stacking relationship with them.
610
+ */
611
+ _restackWindowChildren() {
612
+ const X = this.app?.X;
613
+ if (!this.window || typeof X?.ConfigureWindow !== 'function') return;
614
+ const stack = this._windowStackOrder();
615
+ const ids = stack.map((c) => c.window.id);
616
+ if (
617
+ ids.length === this._xStack.length &&
618
+ ids.every((id, i) => id === this._xStack[i])
619
+ ) {
620
+ return;
621
+ }
622
+ for (let i = stack.length - 2; i >= 0; i--) {
623
+ X.ConfigureWindow(ids[i], {
624
+ sibling: ids[i + 1],
625
+ stackMode: STACK_BELOW,
626
+ });
627
+ }
628
+ this._xStack = ids;
629
+ }
630
+
631
+ insertBefore(child, beforeChild) {
632
+ if (child.isPopup) {
633
+ Node.prototype.insertBefore.call(this, child, beforeChild);
634
+ return;
635
+ }
636
+ if (child.isWindow) {
637
+ const mounting = child.parent == null;
638
+ this._spliceChild(child, beforeChild);
639
+ child.parent = this;
640
+ // Initial children are realized when this window realizes; a child
641
+ // appended to an already-realized window is created immediately,
642
+ // top-down against its real parent — and lands on top of its
643
+ // siblings, which _restackWindowChildren then corrects if the JSX
644
+ // order says otherwise.
645
+ if (this.window && !child.window) {
646
+ child.realize(this.window);
647
+ if (child.window) this._xStack.push(child.window.id);
648
+ }
649
+ if (this.theme || child.props.theme) child._themeChanged(mounting);
650
+ // React reorders a keyed list with one insertBefore per moved child;
651
+ // restacking once at the end of the commit skips the intermediate
652
+ // orders, which nobody ever sees.
653
+ pendingRestack.add(this);
654
+ return;
655
+ }
656
+ Node.prototype.insertBefore.call(this, child, beforeChild);
657
+ }
658
+
659
+ removeChild(child) {
660
+ if (child.isWindow) {
661
+ const index = this._indexOfChild(child);
662
+ if (index !== -1) {
663
+ this.children.splice(index, 1);
664
+ this._nonYogaKids--;
665
+ }
666
+ const id = child.window?.id;
667
+ child.parent = null;
668
+ child.destroySubtree();
669
+ if (id != null) this._xStack = this._xStack.filter((w) => w !== id);
670
+ return;
671
+ }
672
+ Node.prototype.removeChild.call(this, child);
673
+ }
674
+
675
+ destroySubtree() {
676
+ if (this.destroyed) return;
677
+ this.destroyed = true;
678
+ clearPendingFrame(this);
679
+ this._pacer.cancel();
680
+ this._unwatchCompositing?.();
681
+ this._unwatchCompositing = null;
682
+ // before endWindowState below, which is the session this is subscribed to
683
+ this._unwatchLoops();
684
+ this._loopNodes.clear();
685
+ this._animating.clear();
686
+ // `useWindowState()`'s listeners, and the raw VisibilityNotify handler
687
+ // it put on the shared connection, which nothing else would take off
688
+ endWindowState(this);
689
+ // Before the owner's next layout pass, which is this same commit: a
690
+ // popup that has gone still holds a subscription to the window it was
691
+ // anchored to, and answering that notification would configure a dead
692
+ // window.
693
+ this._unwatchAnchor();
694
+ // out of the drag registries before the window goes: a drag routed to
695
+ // a dead window would translate against a null _screenOrigin
696
+ forgetTopLevel(this);
697
+ this._dragSession?.cancel();
698
+ for (const child of this.children) child.destroySubtree();
699
+ if (this.window && typeof this.window.destroy === 'function') {
700
+ this.window.destroy();
701
+ }
702
+ this.window = null;
703
+ if (this.yoga) {
704
+ this.yoga.freeRecursive();
705
+ this.yoga = null;
706
+ }
707
+ }
708
+
709
+ applyProps(newProps, oldProps) {
710
+ const before = oldProps ?? this.props;
711
+ assertWindowSize(newProps, this.kind);
712
+ const beforeStyle = this.style;
713
+ const themeChanged = newProps.theme !== before.theme;
714
+ this.props = newProps;
715
+ if (themeChanged) this._themeChanged();
716
+ const style = this._syncStyle(newProps);
717
+ if (Boolean(newProps.trapFocus) !== Boolean(before.trapFocus)) {
718
+ this._syncFocusScope();
719
+ }
720
+ if (newProps.frameRate !== before.frameRate) this._syncFramePolicy();
721
+ // a <window onDrop> is a whole-window dropzone; same edge as Node
722
+ if (hasDropProps(newProps) !== hasDropProps(before)) {
723
+ if (hasDropProps(newProps)) this._registerDropTarget(this);
724
+ else this._forgetDropTarget(this);
725
+ }
726
+ const wnd = this.window;
727
+ if (!wnd) {
728
+ // Not realized yet: refresh creation attributes instead — through the
729
+ // same filter createInstance used, since these are the arguments ntk's
730
+ // constructor will see. Spreading raw props here was the bug behind
731
+ // `ev.preventDefault is not a function` in a <popup>'s onKeyDown: ntk
732
+ // registers any `onFoo` in its creation args as a raw listener, so the
733
+ // handler was called a second time with the native X event.
734
+ this.attributes = {
735
+ ...this.attributes,
736
+ ...windowAttributes(newProps, this.scale),
737
+ };
738
+ // The flag `realize()`'s map will read — set directly, since there is
739
+ // nothing on screen yet for the notification half of `_applyHidden`
740
+ // to be about.
741
+ this.hidden = this._reactHidden || Boolean(newProps.hidden);
742
+ return;
743
+ }
744
+
745
+ if (newProps.title !== before.title) {
746
+ wnd.setTitle?.(newProps.title || '');
747
+ }
748
+ // The colour the server fills a resize with, kept in step with the one
749
+ // the app paints.
750
+ this._syncWindowBackground();
751
+ // a popup is a child of the screen root, not of the node it is written
752
+ // under, so its zIndex means nothing — and its parent here may well be
753
+ // a drawn node with no children to stack
754
+ if (
755
+ (style.zIndex ?? 0) !== (beforeStyle.zIndex ?? 0) &&
756
+ !this.isPopup &&
757
+ this.parent?._restackWindowChildren
758
+ ) {
759
+ pendingRestack.add(this.parent);
760
+ }
761
+ this._applyWindowHints(newProps, before);
762
+ // Position and size part ways below: both are sent to the server, but
763
+ // only a size change re-lays-out — the window's own coordinate space is
764
+ // untouched by where the window sits on screen, so a pointer-tracking
765
+ // popup does not repaint itself per motion.
766
+ // Compared after normalising, so that an app switching between an
767
+ // omitted size and a spelled-out `'auto'` — the same request written two
768
+ // ways — is not a change and does not reset the state below.
769
+ const sizeChanged =
770
+ canonicalSize(newProps.width) !== canonicalSize(before.width) ||
771
+ canonicalSize(newProps.height) !== canonicalSize(before.height);
772
+ // An anchored window's position is the anchor's business, not the
773
+ // app's: `x`/`y` are ignored while `anchor` is set (`_followAnchor`
774
+ // sends the moves), so a commit that changed nothing else is not a
775
+ // configure back to a stale prop.
776
+ const anchored = Boolean(newProps.anchor);
777
+ const movedByProps =
778
+ !anchored && (newProps.x !== before.x || newProps.y !== before.y);
779
+ const geometryChanged = sizeChanged || movedByProps;
780
+ // A size that became a number is the app taking the size over, which is
781
+ // exactly the state `_userSized` names — and one that became `'auto'` is
782
+ // the app handing it back, so the window fits its content again on the
783
+ // next layout even if the user had resized it before.
784
+ //
785
+ // The record moves with it, and an axis still on `'auto'` records the
786
+ // size the window *has*, because that is the one this configure is not
787
+ // about to change. Without that the echo of a one-axis configure would
788
+ // disagree with the record on the other axis and read as somebody else
789
+ // setting the size — locking the window on the app's own update.
790
+ // The comparisons above ran on the raw props — logical against logical
791
+ // — and everything below talks to the server, so it is device from here
792
+ // (`wnd.width`, `_requestedSize` and the ConfigureNotify echo are all
793
+ // device pixels; a logical number among them would misread every user
794
+ // resize as `_userSized`).
795
+ const geo = scaleWindowGeometry(newProps, this.scale);
796
+ if (sizeChanged) {
797
+ this._userSized = false;
798
+ this._requestedSize = {
799
+ width: isAutoSize(geo.width) ? wnd.width : geo.width,
800
+ height: isAutoSize(geo.height) ? wnd.height : geo.height,
801
+ };
802
+ }
803
+ if (geometryChanged) {
804
+ if (typeof wnd.setState === 'function') {
805
+ wnd.setState({
806
+ x: anchored ? undefined : geo.x,
807
+ y: anchored ? undefined : geo.y,
808
+ // `'auto'` is not a geometry ntk can be given: an axis the app has
809
+ // handed back is left alone here and resolved by `_refit()` on the
810
+ // layout this same commit is about to schedule.
811
+ width: isAutoSize(geo.width) ? undefined : geo.width,
812
+ height: isAutoSize(geo.height) ? undefined : geo.height,
813
+ });
814
+ } else {
815
+ if (sizeChanged && !isAutoSize(geo.width) && !isAutoSize(geo.height)) {
816
+ wnd.resize?.(geo.width, geo.height);
817
+ }
818
+ if (movedByProps) {
819
+ wnd.move?.(geo.x, geo.y);
820
+ }
821
+ }
822
+ }
823
+
824
+ // Re-read every commit: the options object is rebuilt by every render,
825
+ // and a moving `at` — a caret — is the whole point of one. Only the
826
+ // *subscription* is conditional, because only the node it hangs off can
827
+ // make it stale.
828
+ if (newProps.anchor?.to !== before.anchor?.to) this._watchAnchor();
829
+ if (anchored) {
830
+ this._followAnchor();
831
+ } else if (before.anchor) {
832
+ this._placedAt = null;
833
+ // A popup that gives up its anchor gives up being hidden by one: it
834
+ // is an ordinary `x`/`y` popup from here, and the configure above
835
+ // has already put it where the app asked.
836
+ const wasLost = this._anchorLost;
837
+ this._anchorLost = false;
838
+ if (wasLost) this._mapNow();
839
+ }
840
+
841
+ // After the geometry above on purpose: a window revealed and moved in
842
+ // the same commit is configured first and mapped second, so it is never
843
+ // on screen at the position it was hidden at.
844
+ if (Boolean(newProps.hidden) !== Boolean(before.hidden)) {
845
+ this._applyHidden();
846
+ }
847
+
848
+ const layoutChanged =
849
+ style !== beforeStyle && applyLayoutStyle(this.yoga, style, beforeStyle);
850
+ // The window's own paint is its background, which covers the whole
851
+ // window — so a change to it is unbounded, and `this` is the right
852
+ // damage. A commit that only changed children reaches here too, though
853
+ // (React updates the parent whenever its child list is rebuilt), and
854
+ // that must not widen the damage those children just recorded.
855
+ const ownPaintChanged = paintPropsChanged(style, beforeStyle);
856
+ // A size change is unbounded — the window's old bounds do not cover the
857
+ // grown area. A style-driven relayout at the same size is bounded by the
858
+ // window itself (its paint covers all of it; NO_DAMAGE alongside a
859
+ // layout change would fall through invalidate's bounds bookkeeping with
860
+ // no rect at all). An x/y-only commit contributes nothing.
861
+ this.invalidate(
862
+ layoutChanged || sizeChanged,
863
+ sizeChanged ? null : ownPaintChanged || layoutChanged ? this : NO_DAMAGE,
864
+ 'props',
865
+ );
866
+ }
867
+
868
+ /**
869
+ * A window is hidden by unmapping it, not by yoga's `display: none`: it is
870
+ * its own layout root, so collapsing it would throw away the arrangement
871
+ * it comes back to — and there is no parent flex line for it to leave.
872
+ * The flag is still recorded, because it is what tells a map that has not
873
+ * gone out yet not to bother.
874
+ */
875
+ setHidden(hidden) {
876
+ this._reactHidden = hidden;
877
+ this._applyHidden();
878
+ }
879
+
880
+ /**
881
+ * Re-derive `this.hidden` from its two writers — the reconciler's flag and
882
+ * the `hidden` prop — and make the window agree. Either saying "hidden"
883
+ * wins, so a `<Suspense>` revealing its content does not map a window the
884
+ * app is holding off screen, and clearing the prop does not map one React
885
+ * still hides.
886
+ */
887
+ _applyHidden() {
888
+ const hidden = this._reactHidden || Boolean(this.props.hidden);
889
+ if (hidden === this.hidden) return;
890
+ this.hidden = hidden;
891
+ // An unmapped window draws nothing, so a loop inside one is frames
892
+ // nobody sees — the same stop the WM's own minimize gets, by the same
893
+ // route, and the flag is kept true so a later VisibilityNotify agrees
894
+ // with it.
895
+ if (this._loopNodes.size) this._loopVisibilityChanged();
896
+ // An unmapped window is off screen however focused the server thinks it
897
+ // is, and a `<popup>` is worse than that: it shares the owner window's
898
+ // keyboard, so a node inside one that is no longer on screen would go on
899
+ // taking keys the owner window is still receiving.
900
+ this._visibilityChanged(!hidden);
901
+ // A map still queued for the end of this commit reads `hidden` when it
902
+ // runs, so there is nothing to send here — and an unmap sent now would
903
+ // do nothing anyway, the server not having mapped the window yet
904
+ // (issue #201).
905
+ if (pendingMaps.has(this)) return;
906
+ if (hidden) {
907
+ // release before the unmap, the order `_followAnchor` uses — X would
908
+ // drop the grab with the viewability anyway, but not on the mock, and
909
+ // an explicit release is one less state to reason about
910
+ if (this.props.grab) this.window?.ungrabPointer?.();
911
+ this.window?.unmap?.();
912
+ } else if (inCommit) {
913
+ // A reveal mid-commit waits for the end of it the way a fresh window's
914
+ // first map does, so anything later in the same commit that hides the
915
+ // window again (React hides a subtree only after mutating it) is
916
+ // known before the map goes out — the same WM race as issue #201.
917
+ pendingMaps.add(this);
918
+ } else {
919
+ this._mapNow();
920
+ }
921
+ }
922
+ }
923
+
924
+ // The rest of WindowNode's methods live with their concerns: the
925
+ // window-only ones beside this file, and its halves of the concerns it
926
+ // shares with Node beside Node's (see install.js).
927
+ installMethods(
928
+ WindowNode,
929
+ WindowCascade,
930
+ WindowQueries,
931
+ WindowAnimation,
932
+ WindowLayoutHost,
933
+ WindowPosition,
934
+ WindowInvalidate,
935
+ WindowScrollBlit,
936
+ WindowPaint,
937
+ WindowListeners,
938
+ WindowHints,
939
+ WindowSize,
940
+ WindowFlush,
941
+ WindowDebugPaint,
942
+ WindowAnchoring,
943
+ WindowCapabilities,
944
+ WindowDropTarget,
945
+ );