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,600 @@
1
+ // What flows down the tree: the theme and its tokens, the reading direction,
2
+ // the inherited text style and the `scale` zoom — and the walks that
3
+ // re-resolve a subtree when one of them moves.
4
+
5
+ import {
6
+ applyLayoutStyle,
7
+ textStyleFrom,
8
+ DEFAULT_TEXT_STYLE,
9
+ localTextStyleChanged,
10
+ resolvedTextDelta,
11
+ TEXT_REMEASURE,
12
+ } from '../styles.js';
13
+ import { Yoga } from '../yoga.js';
14
+ import { scaleOf } from '../scale.js';
15
+ import { baseTheme } from '../palette.js';
16
+ import { reportStyleError, STRICT_TOKENS } from '../errors.js';
17
+ import { DEV } from './util.js';
18
+
19
+ /**
20
+ * The desktop switched between light and dark: every node that inherited its
21
+ * palette rather than being given one has a different one now.
22
+ *
23
+ * `_themeChanged()` drops the cached theme and restyles the tokens; the
24
+ * invalidate is for everything else, above all the window background, which
25
+ * is read from the palette at paint time and belongs to no style object.
26
+ *
27
+ * Widgets do not need this — they read the palette through `useTheme()` and
28
+ * React re-renders them. This is the other route: an app's own
29
+ * `backgroundColor: '$background'`, and the window fill under it.
30
+ */
31
+ export function appearanceChanged(app) {
32
+ // A desktop change arrives whenever the user makes it, which can be while
33
+ // the app is shutting down. An invalidate *schedules* a frame, so a repaint
34
+ // started here would reach the connection a tick after it closed and throw
35
+ // out of the frame clock, where nothing is waiting to catch it.
36
+ if (!app || app.X?._closing) return;
37
+ // A backend that renders native control bezels caches them by every
38
+ // parameter that changes the pixels — except the desktop's accent, which
39
+ // the toolkit reads for itself. The repaint below would blit the old
40
+ // colour back out of that cache, so it is forgotten first.
41
+ app.nativeBezels?.clear?.();
42
+ for (const node of app._rootChildren ?? []) {
43
+ if (node.destroyed) continue;
44
+ node._themeChanged();
45
+ node.root?.invalidate(true, null, 'theme');
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Depth of the `_themeChanged` walk in progress, if any.
51
+ *
52
+ * That walk already visits every node in the subtree and re-resolves each
53
+ * one, so a style swap it performs on the way down must not kick off a
54
+ * second walk of the nodes it is about to reach anyway. Without the guard a
55
+ * theme change over a tree of token-using nodes is quadratic in its depth.
56
+ */
57
+ export let inThemeWalk = 0;
58
+
59
+ /**
60
+ * A node's own `scale` prop as a factor: what it multiplies the scale it
61
+ * inherits by (`Node.scale`). Absent means 1, which is every node in a tree
62
+ * that never mentions it.
63
+ *
64
+ * A zero, a negative or a NaN is a mistake rather than a design — it would
65
+ * lay the subtree out at nothing, or at infinity — and in development it
66
+ * says so where the mistake is, rather than as a blank pane three frames
67
+ * later. Production falls back to 1 for the same reason a bad token keeps
68
+ * the property dropped: a GUI that carries on is worth more than one that
69
+ * dies on a fraction somebody divided by.
70
+ */
71
+ function scaleFactorOf(props, kind) {
72
+ const own = props?.scale;
73
+ if (own === undefined) return 1;
74
+ if (typeof own === 'number' && Number.isFinite(own) && own > 0) return own;
75
+ if (DEV) {
76
+ throw new Error(
77
+ `react-x11: <${kind} scale={${JSON.stringify(own)}}> — a subtree ` +
78
+ 'scale is a positive number, the factor this subtree is zoomed by ' +
79
+ '(2 draws it twice the size, 0.5 half), and leaving it out means 1. ' +
80
+ 'See docs/scale.md, "A subtree of its own".',
81
+ );
82
+ }
83
+ return 1;
84
+ }
85
+
86
+ /** Node's half of what flows down the tree, installed onto `Node.prototype` by node.js. */
87
+ export class NodeCascade {
88
+ /**
89
+ * Device pixels per logical pixel **for this node** — the display scale
90
+ * `createRoot` resolved (src/scale.js), times every `scale` prop between
91
+ * this node and its window. `this.style` and `this.abs` are already
92
+ * device pixels; this is for the values that never pass through a style —
93
+ * a paint constant like the caret's width, or an event coordinate on its
94
+ * way back to logical. A registered element that draws with its own
95
+ * constants multiplies them by this.
96
+ *
97
+ * The `scale` prop is CSS `zoom`, not a transform: it multiplies the
98
+ * inherited factor, and the node's *own* style scales with it. So
99
+ * everything downstream of the style funnel follows with no second
100
+ * mechanism — yoga lays out the scaled numbers like any others, paint
101
+ * reads the scaled style, the caret and the scrollbar read this getter,
102
+ * and text is shaped at the size it will be drawn at rather than
103
+ * rasterized once and stretched (docs/scale.md, "A subtree of its own").
104
+ *
105
+ * **A real X window is its own root.** `<window>` and `<popup>` geometry
106
+ * is the server's, in the display's pixels — `scaleWindowGeometry` reads
107
+ * `scaleOf(app)` directly and a WM sees no zoom — so the cascade stops
108
+ * there and a menu opened from a zoomed card comes up at the app's own
109
+ * size. Everything else inherits, including `<glarea>` and `<foreign>`,
110
+ * whose boxes are laid out by the parent like any other child's.
111
+ *
112
+ * Cached per node, dropped by `_rescaleSubtree` — the same contract
113
+ * `theme` and `direction` have, for the same reason.
114
+ */
115
+ get scale() {
116
+ if (this._scaleCache !== undefined) return this._scaleCache;
117
+ if (this.isWindow) return (this._scaleCache = scaleOf(this.app));
118
+ const base = this.parent ? this.parent.scale : scaleOf(this.app);
119
+ return (this._scaleCache = base * scaleFactorOf(this.props, this.kind));
120
+ }
121
+
122
+ /**
123
+ * The effective scale of this node moved — its own `scale` prop changed,
124
+ * or it was attached under an ancestor whose scale is not the one it
125
+ * resolved against while detached.
126
+ *
127
+ * Everything below inherits, so the whole subtree is restyled; the early
128
+ * out is the answer not having moved, which is what makes the call at
129
+ * each of the attach sites free in the overwhelmingly common case of a
130
+ * tree with no `scale` prop in it at all.
131
+ *
132
+ * `mounting` is `insertBefore` attaching a subtree that has never been in
133
+ * the tree, and means the same thing it means to `_themeChanged`: resolve
134
+ * everything, claim nothing (issue #402). A node that has never painted
135
+ * has no stale pixels to cover, and where it lands is claimed by the
136
+ * child-list protocol.
137
+ */
138
+ _rescaleSubtree(mounting = false) {
139
+ if (!this._rescaleMoved()) return;
140
+ // One claim for the whole walk: `_invalidateLayout` bounds the subtree
141
+ // as it stands and queues the after-layout claim, so the per-node
142
+ // repeats the recursion below would make are the same rect over again.
143
+ if (!mounting) this._invalidateLayout('scale');
144
+ this._rescaled(mounting);
145
+ }
146
+
147
+ /** Drop the cached scale and re-ask; true when the answer moved. The
148
+ * getter is the only place the rule lives, so a `<window>` — which
149
+ * resolves the display scale whatever it is written inside — answers
150
+ * false here and the walk stops at it. */
151
+ _rescaleMoved() {
152
+ const before = this._scaleCache;
153
+ this._scaleCache = undefined;
154
+ return this.scale !== before;
155
+ }
156
+
157
+ /** …the walk itself, for a node whose scale is already known to have
158
+ * moved. Restyle in the new unit, then carry it down: an ordinary
159
+ * descendant's scale is a product of this one, so it moved too. */
160
+ _rescaled(mounting) {
161
+ // both hold a size in device pixels at the old scale
162
+ this._textBase = undefined;
163
+ this._textScaled = null;
164
+ const prevStyle = this.style;
165
+ const style = this._syncStyle(this.props, mounting);
166
+ if (this.yoga && style !== prevStyle) {
167
+ applyLayoutStyle(this.yoga, style, prevStyle);
168
+ }
169
+ if (localTextStyleChanged(style, prevStyle)) this._textContentChanged();
170
+ // its own claims are bounded, so this runs on a mount too — the same
171
+ // rule `_themeChanged` follows
172
+ this._retext();
173
+ for (const child of this.children) {
174
+ if (child._rescaleMoved()) child._rescaled(mounting);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * The theme in force here: the nearest `theme` prop at or above this node,
180
+ * with an inner one merged over the outer so a panel can restate a colour
181
+ * or two without repeating a palette. Popups resolve through their place
182
+ * in the *tree*, not their window, so a menu inherits the theme of the UI
183
+ * that opened it even though it is a separate X window.
184
+ *
185
+ * **With no `theme` prop anywhere above, it is the desktop's palette.** So
186
+ * `backgroundColor: '$background'` works in an app that never wrote a
187
+ * `<ThemeProvider>`, and means "whatever this desktop's is" — which is the
188
+ * same answer `useTheme()` gives the widgets, by the other route.
189
+ *
190
+ * A detached node has no ancestors yet and so cannot see a provider two
191
+ * levels up; it still resolves, against the base, and `_themeChanged()` on
192
+ * attach re-resolves it against the real one.
193
+ */
194
+ get theme() {
195
+ if (this._theme !== undefined) return this._theme;
196
+ const inherited = this.parent ? this.parent.theme : baseTheme();
197
+ const own = this.props.theme;
198
+ this._theme = own ? { ...inherited, ...own } : inherited;
199
+ return this._theme;
200
+ }
201
+
202
+ /**
203
+ * Which way this node reads: `'ltr'` or `'rtl'`, never `'inherit'` — this
204
+ * is the *resolved* answer, which is what everything outside yoga needs.
205
+ *
206
+ * Yoga resolves the same question for the layout on its own, and does it
207
+ * inside WASM where nothing can read it back (the binding has no
208
+ * `getComputedDirection`). So it is resolved a second time here, over the
209
+ * same rule, for everything the box tree does not answer: which side a
210
+ * scrollbar sits on, which physical edge a `borderStartWidth` paints, which
211
+ * way a popup flips, and the base direction a paragraph of neutral
212
+ * characters resolves against.
213
+ *
214
+ * The rule, nearest first:
215
+ *
216
+ * 1. `direction` in this node's own style — CSS's property, and the one
217
+ * thing that means "this subtree, whatever is around it".
218
+ * 2. otherwise the enclosing element's, which is what makes it inherit.
219
+ * 3. otherwise the palette's, which is seeded from the locale — so an app
220
+ * started in an RTL locale is mirrored without being asked, and
221
+ * `<ThemeProvider value={{ direction }}>` is how one with a language
222
+ * menu says otherwise. The provider plants the matching style property
223
+ * as it goes, so rule 1 is what actually carries a mid-tree swap and
224
+ * this clause is only ever read at the top of the tree.
225
+ *
226
+ * Cached like `theme` and dropped by the same walk, since the two now move
227
+ * together.
228
+ */
229
+ get direction() {
230
+ if (this._direction !== undefined) return this._direction;
231
+ const own = this.style.direction;
232
+ return (this._direction =
233
+ own === 'ltr' || own === 'rtl'
234
+ ? own
235
+ : this.parent
236
+ ? this.parent.direction
237
+ : this.theme.direction === 'rtl'
238
+ ? 'rtl'
239
+ : 'ltr');
240
+ }
241
+
242
+ /**
243
+ * The resolved direction moved — because this node's style named a new one,
244
+ * or because the palette under the whole tree did. Everything below
245
+ * inherits it, so the caches go with it, and the walk stops where a
246
+ * subtree states a direction of its own: nothing under that node can have
247
+ * changed.
248
+ *
249
+ * A node that never resolved one has nothing cached below it either —
250
+ * `direction` fills every ancestor on the way up — which is the same
251
+ * early-out the text cascade takes.
252
+ */
253
+ _redirectSubtree() {
254
+ if (this._direction === undefined) return;
255
+ const before = this._direction;
256
+ this._direction = undefined;
257
+ if (this.direction === before) return;
258
+ this._directionMoved();
259
+ for (const child of this.children) child._redirectSubtree();
260
+ }
261
+
262
+ /**
263
+ * What a direction change costs this node. The default is a repaint: the
264
+ * *layout* has already been dealt with by yoga, which was told about the
265
+ * style property directly, so what is left here is everything painted from
266
+ * the resolved side — the scrollbar, a logical border, an icon.
267
+ *
268
+ * `TextNode` overrides it: a paragraph's base direction is part of how it
269
+ * is shaped, so its cached layouts have to go.
270
+ */
271
+ _directionMoved() {
272
+ this.root?.invalidate(false, this, 'direction');
273
+ }
274
+
275
+ /**
276
+ * The text style this node inherits — **the enclosing element's**, and at
277
+ * the top of the tree the palette's.
278
+ *
279
+ * The ink, the face and the size travel down the tree the way they do in
280
+ * CSS: `<box style={{ color: theme.textMuted, fontSize: 12 }}>` is how a caption
281
+ * block, a disabled row or a code panel is written, and it is what makes
282
+ * `color` on a row reach the row's label without the row handing it over.
283
+ * Only the properties in `INHERITED_TEXT_PROPS` travel; a style property on
284
+ * the node itself still wins, the way it always has.
285
+ *
286
+ * Under the last element is the palette, and that floor is not a set of
287
+ * constants either. The ink first: a `<text>` that never mentions a colour
288
+ * has to be readable on the surface it is drawn on, and that surface
289
+ * follows the desktop now. Black on `#1e2228` is invisible, which is the
290
+ * whole bug. The face and the size for the same reason one step out — a
291
+ * theme names `fontFamily` and `fontSize` because it is describing the type
292
+ * this app sets, and the only way that can be true is if the text nobody
293
+ * styled follows them.
294
+ *
295
+ * A detached node has no parent yet and so resolves against the floor;
296
+ * `insertBefore` re-resolves the subtree against the real one.
297
+ */
298
+ get inheritedTextStyle() {
299
+ if (this.parent) {
300
+ const inherited = this.parent.resolvedTextStyle();
301
+ // A **scale boundary** — this node's `scale` prop, or a `<popup>`
302
+ // written inside a zoomed subtree, which goes back to the display's
303
+ // own unit. What comes down the cascade is already device pixels at
304
+ // the parent's scale (that is the point of resolving the theme's size
305
+ // once, at the root), so re-expressing it here is the only place a
306
+ // second multiply is right: a `fontSize: 14` theme inside a
307
+ // `scale={2}` box is 28 logical, 28 device at 1x, and the descendants
308
+ // below inherit that without compounding it again.
309
+ const from = this.parent.scale;
310
+ const to = this.scale;
311
+ if (from === to) return inherited;
312
+ const size = (inherited.size * to) / from;
313
+ const cached = this._textScaled;
314
+ if (cached?.from !== inherited || cached.style.size !== size) {
315
+ this._textScaled = { from: inherited, style: { ...inherited, size } };
316
+ }
317
+ return this._textScaled.style;
318
+ }
319
+ const theme = this.theme;
320
+ const color = theme.text;
321
+ // A palette can reach a node as a bare `theme` **prop** rather than a
322
+ // resolved one — `<box theme={{ text: 'red' }}>` merges and derives
323
+ // nothing (styling.md) — so neither of these is guaranteed to be there.
324
+ const family = theme.fontFamily ?? DEFAULT_TEXT_STYLE.family;
325
+ // The theme thinks in logical pixels like every style does, and this is
326
+ // the one door its font size enters the cascade by: a node's own
327
+ // `fontSize` was scaled at the style funnel, and every descendant
328
+ // inherits an already-resolved (device) size — so multiplying here,
329
+ // exactly once at the root of the cascade, is what keeps text and
330
+ // layout in the same unit without ever double-scaling (src/scale.js).
331
+ const size = (theme.fontSize ?? DEFAULT_TEXT_STYLE.size) * this.scale;
332
+ const base = this._textBase;
333
+ if (base?.color !== color || base.family !== family || base.size !== size) {
334
+ this._textBase = { ...DEFAULT_TEXT_STYLE, color, family, size };
335
+ }
336
+ return this._textBase;
337
+ }
338
+
339
+ /**
340
+ * Re-resolve this node's text style and pay for what moved.
341
+ *
342
+ * The cache is dropped rather than patched, and the *values* decide what
343
+ * happens next: a node that names its own `color` and face absorbs an
344
+ * ancestor's change entirely, which is what lets `_retextSubtree` stop
345
+ * walking there. Returns the cost so it can.
346
+ *
347
+ * A node with no cached resolution has never been asked, and neither has
348
+ * anything below it — `inheritedTextStyle` fills every ancestor on the way
349
+ * up, so an empty cache here proves an empty cache in the whole subtree.
350
+ * That is the early-out that keeps a hover on one row from touching a
351
+ * window's worth of nodes.
352
+ */
353
+ _retext() {
354
+ const before = this._resolvedText;
355
+ if (before === undefined) return 0;
356
+ this._resolvedText = undefined;
357
+ const cost = resolvedTextDelta(before, this.resolvedTextStyle());
358
+ if (cost !== 0) this._textStyleMoved(cost);
359
+ return cost;
360
+ }
361
+
362
+ /** …and everything under it, stopping wherever the answer did not change. */
363
+ _retextSubtree() {
364
+ if (this._retext() === 0) return;
365
+ for (const child of this.children) child._retextSubtree();
366
+ }
367
+
368
+ /**
369
+ * This node's resolved text style moved — because its own style did, or
370
+ * because an ancestor's did and it inherited the change.
371
+ *
372
+ * `TEXT_REMEASURE` means a glyph can have moved and the box has to be
373
+ * measured again; `TEXT_REPAINT` means only the ink or the glyph rounding
374
+ * did, so the cached layout still has to go — the value rides on the spans
375
+ * inside it — but nothing reflows. Keeping those apart is the whole reason
376
+ * a `:hover { color }` costs a repaint rather than a layout pass.
377
+ *
378
+ * The default serves any element that draws text: re-measure if it has a
379
+ * size of its own, repaint otherwise. Elements that draw no text override
380
+ * it away.
381
+ */
382
+ _textStyleMoved(cost) {
383
+ if (cost === TEXT_REMEASURE && this._measureFn) {
384
+ this.invalidateMeasure('text');
385
+ } else {
386
+ this.root?.invalidate(false, this, 'text');
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Whether this node's ancestry is complete, so a `$token` that does not
392
+ * resolve is a mistake rather than a node that has not been placed yet.
393
+ *
394
+ * For a drawn node that is `root` — set by `_setRoot` when the subtree is
395
+ * attached to the window that owns it, which is exactly when no further
396
+ * `theme` prop can appear above it. React builds bottom-up, so having a
397
+ * *parent* proves nothing: the parent may itself be floating.
398
+ *
399
+ * The exception is a `<popup>`, which is its own root from the moment it is
400
+ * created and only learns where in the tree it was written when it is
401
+ * attached. Until then its subtree would be judged against the base palette
402
+ * alone, and a `$panel` from the provider two levels up would throw. Those
403
+ * resolve provisionally instead, and `_themeChanged()` on attach re-resolves
404
+ * them. Known tokens resolve either way, so only the *error* is ever
405
+ * deferred, never the value.
406
+ */
407
+ get placed() {
408
+ const owner = this.isWindow ? this : this.root;
409
+ if (!owner) return false;
410
+ return owner.isPopup ? owner.parent != null : true;
411
+ }
412
+
413
+ /**
414
+ * A `$token` this node's completed ancestry does not define.
415
+ *
416
+ * The default is `reportStyleError`: say so loudly, set `process.exitCode`,
417
+ * and keep the property dropped. `REACT_X11_STRICT_TOKENS=1` makes it fatal
418
+ * again, and then *where* the throw lands is the whole question — an error
419
+ * boundary only catches what React invoked, on the fiber React thinks it
420
+ * is working on.
421
+ *
422
+ * `mounting` is the attach walk, which runs inside `appendInitialChild`
423
+ * while React is completing the nearest host *ancestor* — the `<window>`,
424
+ * for a whole tree rendered at once. A throw there is attributed to the
425
+ * window and sails past every boundary the app wrote inside it, which is
426
+ * the bug this deferral exists for (#420). Stashed instead, and thrown
427
+ * from `commitMount` on this node's own fiber, where the walk up finds a
428
+ * boundary at any depth.
429
+ *
430
+ * Every other caller already has the right fiber (`commitUpdate`) or has
431
+ * no React on the stack at all (`appearanceChanged`, from an X event) —
432
+ * for those, throwing here is both the earliest and the only option, and
433
+ * the second is the crash strict mode asked for.
434
+ *
435
+ * `commitMount` happens once per instance, so a node re-attached after it
436
+ * has been and gone has nothing left to defer *to*; stashing there would
437
+ * swallow the error instead of raising it late. Those throw at once, like
438
+ * the keyed reorder they resemble.
439
+ */
440
+ _tokenProblem(problems, mounting, consequence = undefined) {
441
+ if (!STRICT_TOKENS) {
442
+ // every one of them: two misspellings in a style are two things to
443
+ // fix, and a report that named only the first would send someone back
444
+ // for a second run to find the second
445
+ for (const message of problems) {
446
+ reportStyleError(this, message, consequence);
447
+ }
448
+ return;
449
+ }
450
+ const error = new Error(problems[0]);
451
+ if (mounting && this._tokenError === null) this._tokenError = error;
452
+ else throw error;
453
+ }
454
+
455
+ /** The theme above or on this node changed: drop the caches and restyle
456
+ * the subtree, since a token can appear at any depth.
457
+ *
458
+ * `mounting` is `insertBefore` attaching a subtree that has never been in
459
+ * the tree: the walk still resolves every token — the nodes can see their
460
+ * ancestors now — but it claims no damage (issue #402). A node that has
461
+ * never painted has no stale pixels to cover, and the rect it is about to
462
+ * occupy is claimed by the child-list/layout-diff protocol like any other
463
+ * inserted child's; the unbounded claims below would turn every commit
464
+ * that mounts a token-styled node into a full-window repaint — which is
465
+ * every re-slice of a virtualized list whose rows follow the palette. A
466
+ * live theme *swap* is the other caller and keeps them: it moves pixels
467
+ * that are already on screen, anywhere in the subtree. */
468
+ _themeChanged(mounting = false) {
469
+ // This walk visits every node itself, so the per-node re-resolution below
470
+ // is enough — a style swap it causes must not start a second walk of the
471
+ // same subtree from halfway down.
472
+ inThemeWalk++;
473
+ try {
474
+ this._theme = undefined;
475
+ // The palette is the floor under the direction too, and this walk
476
+ // already visits every node — so the cache is dropped here rather than
477
+ // through `_redirectSubtree`, which would walk the same subtree again.
478
+ const wasDirection = this._direction;
479
+ this._direction = undefined;
480
+ // A `<window>` with no `backgroundColor` of its own follows the palette,
481
+ // and the server's copy of that colour has to follow with it — otherwise
482
+ // the next resize fills the new area in the old scheme.
483
+ if (this.isWindow) this._syncWindowBackground();
484
+ if (this._usesTokens) {
485
+ const before = this.style;
486
+ this._syncStyle(this.props, mounting);
487
+ // a token change reaches the node without React re-rendering it, so
488
+ // the invalidation a commit would have done has to happen here too
489
+ if (localTextStyleChanged(this.style, before)) {
490
+ this._textContentChanged();
491
+ }
492
+ if (!mounting) this.root?.invalidate(true, null, 'theme');
493
+ }
494
+ // The palette is the floor under the cascade, so a theme swap moves the
495
+ // resolved style of every node that named none of its own — and none of
496
+ // that is in a style object, so nothing above would have noticed. A
497
+ // swap that only changes `fontFamily` is the case that made this worth
498
+ // having: nothing else about the node changes, and a cached layout
499
+ // carries the face it was shaped with. `_retext` runs on a mount too —
500
+ // its own claims are bounded — but cannot answer non-zero there: a
501
+ // node that was never attached has never resolved a text style.
502
+ if (this._retext() !== 0 && !mounting) {
503
+ this.root?.invalidate(true, null, 'theme');
504
+ }
505
+ if (wasDirection !== undefined && this.direction !== wasDirection) {
506
+ this._directionMoved();
507
+ }
508
+ for (const child of this.children) child._themeChanged(mounting);
509
+ } finally {
510
+ inThemeWalk--;
511
+ }
512
+ }
513
+
514
+ /**
515
+ * The text style this node resolves to, in the shape `app.fonts.layout`
516
+ * takes as its base: `{ family, size, weight, style, variations,
517
+ * textRendering, color }`. `<text>`, `<textinput>` and the document views
518
+ * draw with exactly this, and an element that draws text of its own is
519
+ * asking the same question they are.
520
+ *
521
+ * Two things are folded in that `this.style` does not carry. The palette
522
+ * is under it — `text`, `fontFamily` and `fontSize` are the ink, the face
523
+ * and the size of everything that named none of its own (styling.md) — so
524
+ * an element reading its own style alone is one whose app can say
525
+ * `<ThemeProvider value={{ fontFamily: 'Inter' }}>` and watch it reach
526
+ * every built-in label and stop at this one. And the bag is spelled ntk's
527
+ * way rather than the style vocabulary's (`family`, not `fontFamily`;
528
+ * `variations`, not `fontVariationSettings`), which is a mapping worth
529
+ * having in one place instead of vendored per element.
530
+ *
531
+ * **Cached**, and the cache is the cascade's spine: asking here fills every
532
+ * ancestor's on the way up, which is what lets an invalidation walk stop at
533
+ * a node that never resolved (`_retext`). Everything that can move the
534
+ * answer drops it — a style swap (`_retarget`), an animation tick, a theme
535
+ * change, an attach — so an element may keep reading it at paint time.
536
+ */
537
+ resolvedTextStyle() {
538
+ return (this._resolvedText ??= textStyleFrom(
539
+ this.style,
540
+ this.inheritedTextStyle,
541
+ ));
542
+ }
543
+ }
544
+
545
+ /** WindowNode's half of what flows down the tree, installed onto `WindowNode.prototype` by window/window.js. */
546
+ export class WindowCascade {
547
+ /**
548
+ * The smallest size this window's content can be drawn at: GTK's
549
+ * `minimum` to the `natural` below, and what an `'auto'` `minWidth` or
550
+ * `minHeight` resolves to.
551
+ *
552
+ * One layout pass **with no space on offer at all**, which is the whole
553
+ * trick — every node comes out at the smallest size its own style allows,
554
+ * text measures at its longest word, and a wrapping row wraps at every
555
+ * item. `contentSpan` then reads how far that reached, recovering a node
556
+ * the pass squashed by looking inside it.
557
+ *
558
+ * What it deliberately does *not* do is second-guess that layout. A node
559
+ * that said how small it can be — `minWidth: 0`, or an `overflow` that
560
+ * clips — is taken at its word and its content stops counting. That is
561
+ * CSS's `min-width: 0`, Qt's `QScrollArea` and GTK's `min-content-width`,
562
+ * and a scroll container gets it here for free.
563
+ *
564
+ * `forWidth` is the width the height is measured for, and there has to be
565
+ * one: a paragraph's minimum height is a height *for a width*. GTK asks
566
+ * for its minimum height at its minimum width, which for a paragraph is
567
+ * the width it is tallest at — an honest answer to a question nobody
568
+ * asked, since the window is not at its minimum width. `WM_NORMAL_HINTS`
569
+ * holds two independent numbers and cannot express the dependency either
570
+ * way, so the floor is measured at the width the window will actually
571
+ * have and re-sent as that changes.
572
+ */
573
+ /**
574
+ * The direction to lay this window's tree out in, as yoga spells it.
575
+ *
576
+ * `calculateLayout`'s third argument is the direction the *owner* imposes,
577
+ * and a window has no owner — so the root reads its own resolved value and
578
+ * hands it down. A `direction` on a `<box>` inside is then yoga's business
579
+ * rather than ours: it carries the property on its own node and everything
580
+ * under it inherits from there.
581
+ */
582
+ get _rootDirection() {
583
+ return this.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
584
+ }
585
+
586
+ /**
587
+ * The root's direction is an argument to `calculateLayout` rather than a
588
+ * property on a yoga node, so nothing in the box tree is dirty when it
589
+ * moves and the layout pass has to be asked for. That is only reachable
590
+ * from the palette — a `direction` written in the window's own style goes
591
+ * through `applyLayoutStyle`, which dirties the node the ordinary way.
592
+ */
593
+ _directionMoved() {
594
+ this.invalidate(true, null, 'direction');
595
+ }
596
+
597
+ /** Like a `<box>`: a window fills and never letters, so it is the top of
598
+ * the cascade rather than a reader of it. */
599
+ _textStyleMoved() {}
600
+ }