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