react-x11 2.15.2 → 2.16.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.
- package/README.md +37 -0
- package/package.json +4 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/application.js +25 -1
- package/src/capabilities.js +349 -0
- package/src/cocoa/app.js +28 -9
- package/src/cocoa/context2d.js +139 -6
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +160 -0
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/globalmenu.js +3 -205
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +98 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +11 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +65 -2
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +752 -0
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +193 -29
- package/src/types/capabilities.d.ts +139 -0
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/launcher.d.ts +50 -4
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +64 -6
package/src/nodes/image.js
CHANGED
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
PictureSource,
|
|
7
7
|
acquireImageSource,
|
|
8
8
|
decodeImageSource,
|
|
9
|
+
freeImage,
|
|
9
10
|
imageSourceChanged,
|
|
10
11
|
isDirectImageSource,
|
|
11
12
|
isPathImageSource,
|
|
12
13
|
isRawImageSource,
|
|
14
|
+
isSymbolImageSource,
|
|
13
15
|
releaseImageSource,
|
|
14
16
|
toLoadablePath,
|
|
15
17
|
validateImageProps,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
// is a *load-time* SyntaxError, which would take the renderer down rather
|
|
20
22
|
// than the one feature that needs it.
|
|
21
23
|
import * as ntk from 'ntk';
|
|
24
|
+
import { symbolWeight, symbolsFor, warnOnce } from '../symbols.js';
|
|
22
25
|
import { intrinsicSize } from './layout.js';
|
|
23
26
|
import { Node } from './node.js';
|
|
24
27
|
import { DEV } from './util.js';
|
|
@@ -41,6 +44,8 @@ export class ImageNode extends Node {
|
|
|
41
44
|
this._ownedImage = null;
|
|
42
45
|
/** PictureSource/DrawableSource, when the source is server-side */
|
|
43
46
|
this._serverSource = null;
|
|
47
|
+
/** `{ symbol, … }`, when the source is a name the platform draws */
|
|
48
|
+
this._symbol = null;
|
|
44
49
|
// Resolution waits for the first layout/paint: the constructor runs in
|
|
45
50
|
// the render phase, which React may discard, and resolving here would
|
|
46
51
|
// start file reads and take cache holds nothing would ever release.
|
|
@@ -66,6 +71,25 @@ export class ImageNode extends Node {
|
|
|
66
71
|
measureContent(constraints) {
|
|
67
72
|
this._ensureSource();
|
|
68
73
|
const s = this.scale;
|
|
74
|
+
if (this._symbol) {
|
|
75
|
+
const name = this._symbol.symbol;
|
|
76
|
+
const size = symbolsFor(this.app).size(name, this._symbolOptions());
|
|
77
|
+
// A name this desktop does not have takes no room and draws nothing,
|
|
78
|
+
// which is right for an app that runs on both and wrong for a typo —
|
|
79
|
+
// development tells the two apart for it.
|
|
80
|
+
if (!size) {
|
|
81
|
+
warnOnce(
|
|
82
|
+
`react-x11: <image src={{ symbol: ${JSON.stringify(name)} }}> is ` +
|
|
83
|
+
'not a symbol this desktop has, so it takes no room and draws ' +
|
|
84
|
+
'nothing. SF Symbols are the names on macOS, and the icon ' +
|
|
85
|
+
"theme's names, like 'audio-volume-high', elsewhere.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return intrinsicSize(
|
|
89
|
+
{ width: (size?.width ?? 0) * s, height: (size?.height ?? 0) * s },
|
|
90
|
+
constraints,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
69
93
|
return intrinsicSize(
|
|
70
94
|
{
|
|
71
95
|
width: (this.image?.width ?? 0) * s,
|
|
@@ -95,6 +119,12 @@ export class ImageNode extends Node {
|
|
|
95
119
|
return;
|
|
96
120
|
}
|
|
97
121
|
if (src == null) return;
|
|
122
|
+
if (isSymbolImageSource(src)) {
|
|
123
|
+
// nothing to load: the platform draws the name at paint, which is also
|
|
124
|
+
// when the text colour it is drawn in is known
|
|
125
|
+
this._symbol = src;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
98
128
|
if (isDirectImageSource(src)) {
|
|
99
129
|
// the caller's object — its upload cache is the dedupe, and it is
|
|
100
130
|
// never destroyed here
|
|
@@ -215,16 +245,36 @@ export class ImageNode extends Node {
|
|
|
215
245
|
}
|
|
216
246
|
if (this._ownedImage) {
|
|
217
247
|
// frees the per-app upload; the caller's own Images are never here
|
|
218
|
-
this._ownedImage
|
|
248
|
+
freeImage(this.app, this._ownedImage);
|
|
219
249
|
this._ownedImage = null;
|
|
220
250
|
}
|
|
221
251
|
if (this._serverSource) {
|
|
222
252
|
this._serverSource.destroy?.();
|
|
223
253
|
this._serverSource = null;
|
|
224
254
|
}
|
|
255
|
+
this._symbol = null;
|
|
225
256
|
this.image = null;
|
|
226
257
|
}
|
|
227
258
|
|
|
259
|
+
/**
|
|
260
|
+
* How a symbol is drawn beside the text around it: at that text's size and
|
|
261
|
+
* weight unless the source says otherwise — what SF Symbols are designed
|
|
262
|
+
* for, and what lets a toolbar of them follow a theme's `fontSize` — in its
|
|
263
|
+
* colour, which is `currentColor` for an `<svg>` too. Sizes are logical.
|
|
264
|
+
*/
|
|
265
|
+
_symbolOptions() {
|
|
266
|
+
const text = this.resolvedTextStyle();
|
|
267
|
+
const src = this._symbol;
|
|
268
|
+
return {
|
|
269
|
+
pointSize: text.size / this.scale,
|
|
270
|
+
weight: symbolWeight(src.weight ?? text.weight),
|
|
271
|
+
scale: src.scale,
|
|
272
|
+
variableValue: src.variableValue,
|
|
273
|
+
displayScale: this.scale,
|
|
274
|
+
color: text.color,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
228
278
|
applyProps(newProps, oldProps) {
|
|
229
279
|
const before = oldProps ?? this.props;
|
|
230
280
|
const sourceChanged = imageSourceChanged(newProps, before);
|
|
@@ -233,12 +283,16 @@ export class ImageNode extends Node {
|
|
|
233
283
|
super.applyProps(newProps, oldProps);
|
|
234
284
|
if (!sourceChanged) return;
|
|
235
285
|
const prev = this.image;
|
|
286
|
+
const wasSymbol = this._symbol;
|
|
236
287
|
this._releaseSource();
|
|
237
288
|
this._sourceDirty = false;
|
|
238
289
|
this._resolveSource();
|
|
239
290
|
// paintChanged already claimed this node's box through super; only a
|
|
240
|
-
// new intrinsic size needs more than that
|
|
291
|
+
// new intrinsic size needs more than that — and a symbol's size is the
|
|
292
|
+
// platform's to say, so any change to one is measured again
|
|
241
293
|
if (
|
|
294
|
+
wasSymbol ||
|
|
295
|
+
this._symbol ||
|
|
242
296
|
(prev?.width ?? 0) !== (this.image?.width ?? 0) ||
|
|
243
297
|
(prev?.height ?? 0) !== (this.image?.height ?? 0)
|
|
244
298
|
) {
|
|
@@ -253,6 +307,15 @@ export class ImageNode extends Node {
|
|
|
253
307
|
|
|
254
308
|
paintContent(ctx) {
|
|
255
309
|
this._ensureSource();
|
|
310
|
+
if (this._symbol) {
|
|
311
|
+
symbolsFor(this.app).draw(
|
|
312
|
+
ctx,
|
|
313
|
+
this._symbol.symbol,
|
|
314
|
+
this.contentBox(),
|
|
315
|
+
this._symbolOptions(),
|
|
316
|
+
);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
256
319
|
if (!this.image) return;
|
|
257
320
|
const content = this.contentBox();
|
|
258
321
|
ctx.drawImage(
|
package/src/nodes/kinds.js
CHANGED
|
@@ -29,3 +29,15 @@ export const CUSTOM_SEMANTIC_NAMES = new Map();
|
|
|
29
29
|
* arrangement as above, for the other declaration a scene-drawing element
|
|
30
30
|
* makes (issue #301). */
|
|
31
31
|
export const CUSTOM_SELF_DAMAGED = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The element `<ThemeProvider>` renders to carry its palette into the node
|
|
35
|
+
* tree. Not one of `HOST_TYPES` and not documented as an element: the
|
|
36
|
+
* provider is the API. What node it becomes depends on where it is written
|
|
37
|
+
* (`createInstance`): inside a window it is a `<box>` that fills its parent —
|
|
38
|
+
* directly inside one, a `ThemeBoxNode` that also hands nested windows on to
|
|
39
|
+
* it — and at the root of the tree, above the windows, where nothing drawn
|
|
40
|
+
* may be, a `ThemeScopeNode` that draws nothing and hands the palette to the
|
|
41
|
+
* windows under it (nodes/scope.js).
|
|
42
|
+
*/
|
|
43
|
+
export const THEME_SCOPE = 'themescope';
|
package/src/nodes/layout.js
CHANGED
|
@@ -284,7 +284,11 @@ export class NodeLayout {
|
|
|
284
284
|
// cached unions all the way up with it
|
|
285
285
|
this._clearHitBounds();
|
|
286
286
|
if (layoutDiff.sink) {
|
|
287
|
-
|
|
287
|
+
// the reach `_ownPaintBounds` names, ring and shadow alike: a card
|
|
288
|
+
// pushed down by a row above it leaves its old shadow on the surface
|
|
289
|
+
// unless the claim for where it was covers that shadow too
|
|
290
|
+
const grow =
|
|
291
|
+
Math.max(this._outlineExtent(), this._shadowExtent()) + DAMAGE_SLOP;
|
|
288
292
|
const shift = layoutDiff.shift;
|
|
289
293
|
const had = old.width > 0 && old.height > 0;
|
|
290
294
|
if (shift) {
|
package/src/nodes/node.js
CHANGED
|
@@ -34,7 +34,7 @@ import { NO_DAMAGE } from './damage.js';
|
|
|
34
34
|
import { NodeHitTest } from './hittest.js';
|
|
35
35
|
import { installMethods } from './install.js';
|
|
36
36
|
import { NodeInvalidate } from './invalidate.js';
|
|
37
|
-
import { CUSTOM_SELF_DAMAGED } from './kinds.js';
|
|
37
|
+
import { CUSTOM_SELF_DAMAGED, THEME_SCOPE } from './kinds.js';
|
|
38
38
|
import { NodeLayout } from './layout.js';
|
|
39
39
|
import { NodeLayoutHost } from './layouthost.js';
|
|
40
40
|
import { NodePaint } from './paint.js';
|
|
@@ -154,6 +154,9 @@ export class Node {
|
|
|
154
154
|
// subtree's hit reach, invalidated through _clearHitBounds()
|
|
155
155
|
this._paintOrderCache = null;
|
|
156
156
|
this._hitBoundsCache = null;
|
|
157
|
+
// the surface an `opacity` below 1 draws this subtree through, kept from
|
|
158
|
+
// one frame to the next while it fits (`NodePaint._paintGroup`)
|
|
159
|
+
this._groupSurface = null;
|
|
157
160
|
// a `$token` the theme does not define, held for `commitMount` to throw
|
|
158
161
|
// on this node's own fiber — see `_tokenProblem`. Strict mode only.
|
|
159
162
|
// `null` is "commitMount is still to come", `false` is "it has been and
|
|
@@ -327,9 +330,19 @@ export class Node {
|
|
|
327
330
|
return;
|
|
328
331
|
}
|
|
329
332
|
if (child.isWindow) {
|
|
333
|
+
// A `<ThemeProvider>` inside a window is drawn as a box, which the
|
|
334
|
+
// developer never wrote — so the message names what they did write.
|
|
335
|
+
// Directly inside a window it passes a nested window on
|
|
336
|
+
// (nodes/scope.js, `ThemeBoxNode`); this is one further down.
|
|
330
337
|
throw new Error(
|
|
331
|
-
|
|
332
|
-
'
|
|
338
|
+
this._reactFiber?.type === THEME_SCOPE
|
|
339
|
+
? 'react-x11: a <window> under this <ThemeProvider> cannot be ' +
|
|
340
|
+
'nested — the provider is inside a <box>, where it is a box ' +
|
|
341
|
+
'itself, and a window nests only in a window. Put the ' +
|
|
342
|
+
'provider directly inside the outer <window>, or inside the ' +
|
|
343
|
+
'nested one.'
|
|
344
|
+
: `react-x11: <window> cannot be nested inside <${this.kind}>; ` +
|
|
345
|
+
'windows may only appear at the root or inside another <window>.',
|
|
333
346
|
);
|
|
334
347
|
}
|
|
335
348
|
// A registered element that declared childrenAllowed: false says so
|
|
@@ -499,6 +512,7 @@ export class Node {
|
|
|
499
512
|
* freed by the caller via freeRecursive on the subtree top. */
|
|
500
513
|
destroySubtree() {
|
|
501
514
|
this.destroyed = true;
|
|
515
|
+
if (this._groupSurface) this._releaseGroupSurface();
|
|
502
516
|
// a loop outlives nothing: the window drops it from the set that keeps
|
|
503
517
|
// its frame clock alive, and stops watching visibility with the last one
|
|
504
518
|
this.root?._forgetLoopNode(this);
|
package/src/nodes/paint.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// window, background first.
|
|
4
4
|
|
|
5
5
|
import { isPlaced } from '../layouts.js';
|
|
6
|
+
import { Surface } from '../ntk.js';
|
|
6
7
|
import { isPaintedColor } from './boxpaint.js';
|
|
7
8
|
import { DAMAGE_SLOP } from './damage.js';
|
|
8
9
|
import { DRAWN_KINDS } from './kinds.js';
|
|
@@ -97,6 +98,122 @@ export class NodePaint {
|
|
|
97
98
|
|
|
98
99
|
paint(ctx) {
|
|
99
100
|
if (this.hidden) return;
|
|
101
|
+
const opacity = this.style.opacity;
|
|
102
|
+
// A retained presenter replaying this node's own paint into a layer puts
|
|
103
|
+
// the opacity on the layer instead (src/cocoa/presenter.js), and a group
|
|
104
|
+
// drawn here as well would fade it twice.
|
|
105
|
+
if (opacity !== undefined && opacity < 1 && !this._ownPaintOnly) {
|
|
106
|
+
// Nothing to see, so nothing to draw — NaN included. The node is still
|
|
107
|
+
// laid out and still hit, as CSS's `opacity: 0` is.
|
|
108
|
+
if (!(opacity > 0)) return;
|
|
109
|
+
this._paintGroup(ctx, opacity);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// back to opaque: the surface a fade drew through is not needed any more
|
|
113
|
+
if (this._groupSurface) this._releaseGroupSurface();
|
|
114
|
+
this._paintNode(ctx);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `opacity` below 1: this node and everything under it drawn **once**, into
|
|
119
|
+
* a surface, and composited at that alpha — so it fades as a group. An icon
|
|
120
|
+
* over its own card does not show the card through the icon, a border does
|
|
121
|
+
* not double up where it meets a background, and text keeps the colour it
|
|
122
|
+
* had. That is CSS's `opacity`, and the reason it is not a `globalAlpha` on
|
|
123
|
+
* each draw.
|
|
124
|
+
*
|
|
125
|
+
* The surface is the subtree's paint bounds inside the window — stable
|
|
126
|
+
* across frames, so a fade reuses it — and only this pass's damage is drawn
|
|
127
|
+
* into it. Where the backend makes no surfaces (the headless mock) the
|
|
128
|
+
* subtree is drawn with the alpha on each draw instead: the same pixels
|
|
129
|
+
* wherever nothing inside it overlaps.
|
|
130
|
+
*/
|
|
131
|
+
_paintGroup(ctx, opacity) {
|
|
132
|
+
const rect = this._groupRect();
|
|
133
|
+
if (!rect) return;
|
|
134
|
+
const surface = this._groupSurfaceFor(rect.width, rect.height);
|
|
135
|
+
const alpha = ctx.globalAlpha ?? 1;
|
|
136
|
+
if (!surface) {
|
|
137
|
+
ctx.globalAlpha = alpha * opacity;
|
|
138
|
+
try {
|
|
139
|
+
this._paintNode(ctx);
|
|
140
|
+
} finally {
|
|
141
|
+
ctx.globalAlpha = alpha;
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
surface.render((sctx) => {
|
|
146
|
+
sctx.clearRect(0, 0, rect.width, rect.height);
|
|
147
|
+
sctx.translate(-rect.x, -rect.y);
|
|
148
|
+
const damage = this.root?._paintDamage;
|
|
149
|
+
if (damage) {
|
|
150
|
+
sctx.beginPath();
|
|
151
|
+
sctx.rect(damage.x, damage.y, damage.width, damage.height);
|
|
152
|
+
sctx.clip();
|
|
153
|
+
}
|
|
154
|
+
this._paintNode(sctx);
|
|
155
|
+
});
|
|
156
|
+
ctx.globalAlpha = alpha * opacity;
|
|
157
|
+
try {
|
|
158
|
+
ctx.drawImage(surface, rect.x, rect.y);
|
|
159
|
+
} finally {
|
|
160
|
+
ctx.globalAlpha = alpha;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Where a group's surface goes: the subtree's paint bounds, on whole
|
|
165
|
+
* pixels, cut to the window — a faded pane scrolled far off the top has
|
|
166
|
+
* nothing of its own down there to keep. Null when none of it is on the
|
|
167
|
+
* window at all. */
|
|
168
|
+
_groupRect() {
|
|
169
|
+
const b = this._subtreeBounds();
|
|
170
|
+
const win = this.root?.abs;
|
|
171
|
+
let x0 = Math.floor(b.x);
|
|
172
|
+
let y0 = Math.floor(b.y);
|
|
173
|
+
let x1 = Math.ceil(b.x + b.width);
|
|
174
|
+
let y1 = Math.ceil(b.y + b.height);
|
|
175
|
+
if (win) {
|
|
176
|
+
x0 = Math.max(x0, 0);
|
|
177
|
+
y0 = Math.max(y0, 0);
|
|
178
|
+
x1 = Math.min(x1, Math.ceil(win.width));
|
|
179
|
+
y1 = Math.min(y1, Math.ceil(win.height));
|
|
180
|
+
}
|
|
181
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
182
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The group surface at this size, kept from the last frame when it fits —
|
|
186
|
+
* a fade asks for the same one every frame. Null where the backend has
|
|
187
|
+
* none to give. */
|
|
188
|
+
_groupSurfaceFor(width, height) {
|
|
189
|
+
const kept = this._groupSurface;
|
|
190
|
+
if (kept && kept.width === width && kept.height === height) return kept;
|
|
191
|
+
if (kept) this._releaseGroupSurface();
|
|
192
|
+
const app = this.app;
|
|
193
|
+
if (!app?.display?.Render && typeof app?.createSurface !== 'function') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
this._groupSurface = new Surface(app, { width, height });
|
|
198
|
+
} catch {
|
|
199
|
+
// no pixmap to be had: the fallback draws with the alpha instead
|
|
200
|
+
this._groupSurface = null;
|
|
201
|
+
}
|
|
202
|
+
return this._groupSurface;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_releaseGroupSurface() {
|
|
206
|
+
const surface = this._groupSurface;
|
|
207
|
+
this._groupSurface = null;
|
|
208
|
+
try {
|
|
209
|
+
surface?.destroy();
|
|
210
|
+
} catch {
|
|
211
|
+
// gone with its connection
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Everything this node draws, and its children: `paint` minus the group. */
|
|
216
|
+
_paintNode(ctx) {
|
|
100
217
|
// Outside the box and under everything, which is the whole of what makes
|
|
101
218
|
// a shadow different from a colour: it is drawn before this node's own
|
|
102
219
|
// background so a translucent background does not sit on top of it, and
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Where a `<ThemeProvider>` meets the windows. At the root of the tree: what
|
|
2
|
+
// the container holds — top-level windows and popups — and the node a
|
|
3
|
+
// provider written above them becomes. And directly inside a window: the box
|
|
4
|
+
// a provider becomes there, which passes a nested window on to that window.
|
|
5
|
+
|
|
6
|
+
import { hooks as a11yHooks } from '../a11y.js';
|
|
7
|
+
import { baseTheme } from '../palette.js';
|
|
8
|
+
import { BoxNode } from './box.js';
|
|
9
|
+
import { THEME_SCOPE } from './kinds.js';
|
|
10
|
+
import { Node } from './node.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Put `node` at the top of the tree: realize a window against the screen
|
|
14
|
+
* root, and record it among the container's top-level windows — the list
|
|
15
|
+
* every "which windows does this app have" question reads
|
|
16
|
+
* (`app._rootChildren`). A theme scope puts its windows there instead of
|
|
17
|
+
* itself, so the list stays one of windows.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent, because React reorders a keyed list at the root by inserting
|
|
20
|
+
* a child that is already mounted: a second entry for the same window would
|
|
21
|
+
* be a window counted twice, and announced to assistive technology twice.
|
|
22
|
+
*/
|
|
23
|
+
export function attachTopLevel(app, node) {
|
|
24
|
+
if (node.isThemeScope) {
|
|
25
|
+
node._attach();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// realize the whole subtree top-down against the screen root
|
|
29
|
+
if (!node.window) node.realize(null);
|
|
30
|
+
// React's getPublicRootInstance answers from the root fiber's first
|
|
31
|
+
// child, and only when that child is a host component. `render()` wraps
|
|
32
|
+
// the tree in a context provider, which is not one, so it would answer
|
|
33
|
+
// null — the container keeps the list instead.
|
|
34
|
+
const roots = (app._rootChildren ??= []);
|
|
35
|
+
if (roots.includes(node)) return;
|
|
36
|
+
roots.push(node);
|
|
37
|
+
a11yHooks.rootMounted?.(node);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Take `node` back off the top of the tree, before its subtree is
|
|
41
|
+
* destroyed — the bridge reads the subtree to say what went. */
|
|
42
|
+
export function detachTopLevel(app, node) {
|
|
43
|
+
if (node.isThemeScope) {
|
|
44
|
+
node._detach();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const roots = app._rootChildren;
|
|
48
|
+
const at = roots ? roots.indexOf(node) : -1;
|
|
49
|
+
if (at !== -1) roots.splice(at, 1);
|
|
50
|
+
a11yHooks.rootUnmounted?.(node);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `<ThemeProvider>` above the windows (#584).
|
|
55
|
+
*
|
|
56
|
+
* A provider has to put its palette on a node, because a `$token` resolves by
|
|
57
|
+
* walking the node tree and knows nothing about React context. Inside a window
|
|
58
|
+
* that node is a `<box>`. At the root it cannot be — nothing drawn can be
|
|
59
|
+
* there, and a window cannot be inside a box — so the provider used to put
|
|
60
|
+
* the palette on the windows it could see among its children instead. It
|
|
61
|
+
* could only see literal `<window>` elements: a component that renders one,
|
|
62
|
+
* a window that is closed, a fragment of two, all planted a box at the root.
|
|
63
|
+
*
|
|
64
|
+
* This is the node for that position. It draws nothing, lays nothing out and
|
|
65
|
+
* holds only windows, popups and other scopes, and the windows under it take
|
|
66
|
+
* their palette from it: a top-level window keeps **no parent** — a window
|
|
67
|
+
* with a parent is a nested one to everything that asks, the accessibility
|
|
68
|
+
* bridge first — and reads the scope through `_scope` instead
|
|
69
|
+
* (`Node.theme`).
|
|
70
|
+
*
|
|
71
|
+
* The scope itself is never in `app._rootChildren`; its windows are, from
|
|
72
|
+
* the moment it is attached to the container (`attachTopLevel`).
|
|
73
|
+
*/
|
|
74
|
+
export class ThemeScopeNode extends Node {
|
|
75
|
+
constructor(props, app) {
|
|
76
|
+
super(THEME_SCOPE, props, app, { yoga: false });
|
|
77
|
+
this.isThemeScope = true;
|
|
78
|
+
// the scope this one is written inside, when providers nest at the root
|
|
79
|
+
this._scope = null;
|
|
80
|
+
// in the container: its windows are realized and on the root list
|
|
81
|
+
this._attached = false;
|
|
82
|
+
// React's hide (`<Suspense>`, `<Activity>`), which lands on the topmost
|
|
83
|
+
// host instance under the boundary — this, when the provider is there
|
|
84
|
+
this._reactHidden = false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The palette the windows under this scope inherit: this scope's own
|
|
89
|
+
* `theme` over the scope around it, or over the desktop's palette.
|
|
90
|
+
*
|
|
91
|
+
* Not cached, unlike `Node.theme`. Nothing tells a scope that the
|
|
92
|
+
* desktop's palette moved — `appearanceChanged` walks the windows — and a
|
|
93
|
+
* cached merge would hand them the old base. Each window caches what it
|
|
94
|
+
* read, so this runs once per window per theme change.
|
|
95
|
+
*/
|
|
96
|
+
get theme() {
|
|
97
|
+
const inherited = this._scope ? this._scope.theme : baseTheme();
|
|
98
|
+
const own = this.props.theme;
|
|
99
|
+
return own ? { ...inherited, ...own } : inherited;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
insertBefore(child, beforeChild) {
|
|
103
|
+
const from = this.children.indexOf(child);
|
|
104
|
+
if (from !== -1) this.children.splice(from, 1);
|
|
105
|
+
const before =
|
|
106
|
+
beforeChild == null ? -1 : this.children.indexOf(beforeChild);
|
|
107
|
+
this.children.splice(
|
|
108
|
+
before === -1 ? this.children.length : before,
|
|
109
|
+
0,
|
|
110
|
+
child,
|
|
111
|
+
);
|
|
112
|
+
// a keyed reorder: the child is already linked, themed and attached
|
|
113
|
+
if (from !== -1) return;
|
|
114
|
+
child._scope = this;
|
|
115
|
+
// Built while detached, the subtree resolved its tokens against the
|
|
116
|
+
// desktop's palette; this is the attach walk that re-resolves them, the
|
|
117
|
+
// same one `Node.insertBefore` runs — a mount, so it claims no damage.
|
|
118
|
+
child._themeChanged(true);
|
|
119
|
+
if (this._hiddenByReact()) child._applyHidden();
|
|
120
|
+
if (this._attached) attachTopLevel(this.app, child);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
removeChild(child) {
|
|
124
|
+
const at = this.children.indexOf(child);
|
|
125
|
+
if (at === -1) return;
|
|
126
|
+
this.children.splice(at, 1);
|
|
127
|
+
if (this._attached) detachTopLevel(this.app, child);
|
|
128
|
+
child._scope = null;
|
|
129
|
+
child.destroySubtree();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
applyProps(newProps, oldProps) {
|
|
133
|
+
const before = oldProps ?? this.props;
|
|
134
|
+
this.props = newProps;
|
|
135
|
+
// A swap walks the windows as a live change, which repaints them. The
|
|
136
|
+
// `style` a provider passes lays out a box inside a window and means
|
|
137
|
+
// nothing here: a direction it names is in the palette as well, which
|
|
138
|
+
// is what a window reads its direction from.
|
|
139
|
+
if (newProps.theme !== before.theme) this._themeChanged();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setHidden(hidden) {
|
|
143
|
+
this._reactHidden = hidden;
|
|
144
|
+
this._applyHidden();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The hidden state above the windows moved: each one re-derives its own. */
|
|
148
|
+
_applyHidden() {
|
|
149
|
+
for (const child of this.children) child._applyHidden();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Whether React hides this scope, here or at a scope around it. */
|
|
153
|
+
_hiddenByReact() {
|
|
154
|
+
return this._reactHidden || (this._scope?._hiddenByReact() ?? false);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_attach() {
|
|
158
|
+
this._attached = true;
|
|
159
|
+
for (const child of this.children) attachTopLevel(this.app, child);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_detach() {
|
|
163
|
+
this._attached = false;
|
|
164
|
+
for (const child of this.children) detachTopLevel(this.app, child);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `<ThemeProvider>` written directly inside a `<window>` (or a `<popup>`).
|
|
170
|
+
*
|
|
171
|
+
* There it is a box that fills its parent, like a provider anywhere else in
|
|
172
|
+
* a window — but a window may nest windows, and a provider wrapped around one
|
|
173
|
+
* has to hand it on, since a window cannot live inside a box. So a nested
|
|
174
|
+
* window put under this box is **passed through** to the window the box is
|
|
175
|
+
* in: that window holds it, realizes and stacks it the way it does any
|
|
176
|
+
* nested window, and the window reads its palette from here through
|
|
177
|
+
* `_scope`, which a palette lookup asks before the parent (`Node.theme`).
|
|
178
|
+
*
|
|
179
|
+
* React still sees the window as this box's child, so the calls it makes on
|
|
180
|
+
* the box for that window — a move, a removal — are forwarded as well, and
|
|
181
|
+
* the box answers for the passed windows in the three things it owns over its
|
|
182
|
+
* subtree: a theme swap, a hide, and its own removal.
|
|
183
|
+
*
|
|
184
|
+
* Only here. Under a provider that is inside a `<box>` there is no window to
|
|
185
|
+
* pass a window to, and `Node.insertBefore` says so.
|
|
186
|
+
*/
|
|
187
|
+
export class ThemeBoxNode extends BoxNode {
|
|
188
|
+
constructor(props, app) {
|
|
189
|
+
super(props, app);
|
|
190
|
+
// the nested windows React put under this box, in the order it did
|
|
191
|
+
this._windows = [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
insertBefore(child, beforeChild) {
|
|
195
|
+
if (!child.isWindow || child.isPopup) {
|
|
196
|
+
super.insertBefore(child, beforeChild);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!this._windows.includes(child)) {
|
|
200
|
+
this._windows.push(child);
|
|
201
|
+
child._scope = this;
|
|
202
|
+
}
|
|
203
|
+
// Before the box is in its window the window waits here, and `_setRoot`
|
|
204
|
+
// passes it on at the attach. A move is re-inserted at the end: nested
|
|
205
|
+
// windows stack among themselves, not among the box's drawn children.
|
|
206
|
+
if (this.parent) this.parent.insertBefore(child, null);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
removeChild(child) {
|
|
210
|
+
const at = this._windows.indexOf(child);
|
|
211
|
+
if (at === -1) {
|
|
212
|
+
super.removeChild(child);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this._windows.splice(at, 1);
|
|
216
|
+
// the window destroys it, and takes it off its stacking list
|
|
217
|
+
if (child.parent) child.parent.removeChild(child);
|
|
218
|
+
else child.destroySubtree();
|
|
219
|
+
child._scope = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_setRoot(root) {
|
|
223
|
+
const attaching = this.root !== root;
|
|
224
|
+
super._setRoot(root);
|
|
225
|
+
if (!attaching || !this.parent) return;
|
|
226
|
+
for (const win of this._windows) {
|
|
227
|
+
if (win.parent !== this.parent) this.parent.insertBefore(win, null);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_themeChanged(mounting = false) {
|
|
232
|
+
super._themeChanged(mounting);
|
|
233
|
+
for (const win of this._windows) win._themeChanged(mounting);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setHidden(hidden) {
|
|
237
|
+
super.setHidden(hidden);
|
|
238
|
+
for (const win of this._windows) win._applyHidden();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** React hid this box — the hide a `<Suspense>` around the provider
|
|
242
|
+
* lands on, which the windows passed on from here have to follow. */
|
|
243
|
+
_hiddenByReact() {
|
|
244
|
+
return this.hidden;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
destroySubtree() {
|
|
248
|
+
super.destroySubtree();
|
|
249
|
+
// React removes the box alone, never the windows under it, so they go
|
|
250
|
+
// with it — out of the window that holds them, which is each one's
|
|
251
|
+
// parent (the box's own is already cleared by now), unless that window
|
|
252
|
+
// is being destroyed itself and is walking its children as it does.
|
|
253
|
+
for (const win of this._windows) {
|
|
254
|
+
const holder = win.parent;
|
|
255
|
+
if (holder && !holder.destroyed) holder.removeChild(win);
|
|
256
|
+
else if (!win.destroyed) win.destroySubtree();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|