react-x11 2.2.0 → 2.3.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 +5 -3
- package/package.json +12 -5
- package/src/Reconciler.js +96 -13
- package/src/anchor.js +15 -5
- package/src/appcontext.js +27 -7
- package/src/cocoa/app.js +557 -0
- package/src/cocoa/bezels.js +161 -0
- package/src/cocoa/context2d.js +679 -0
- package/src/cocoa/fonts.js +541 -0
- package/src/cocoa/glarea.js +321 -0
- package/src/cocoa/globalmenu.js +149 -0
- package/src/cocoa/keymap.js +86 -0
- package/src/cocoa/native.js +51 -0
- package/src/cocoa/panehost.js +50 -0
- package/src/cocoa/panewindow.js +236 -0
- package/src/cocoa/presenter.js +837 -0
- package/src/cocoa/window.js +383 -0
- package/src/components/Button.js +68 -0
- package/src/components/Checkbox.js +43 -0
- package/src/components/Radio.js +37 -1
- package/src/components/Select.js +73 -28
- package/src/components/Slider.js +59 -0
- package/src/components/Switch.js +52 -0
- package/src/components/native.js +142 -0
- package/src/decorations.js +21 -24
- package/src/editmenu.js +23 -14
- package/src/errors.js +42 -0
- package/src/events.js +11 -2
- package/src/frame/childmain.js +9 -0
- package/src/frame/index.js +162 -2
- package/src/glnodes.js +43 -0
- package/src/globalmenu.js +11 -3
- package/src/index.d.ts +10 -0
- package/src/nodes.js +165 -37
- package/src/paintcache.js +15 -4
- package/src/palette.js +14 -0
- package/src/style.d.ts +4 -1
- package/src/styles.js +48 -19
- package/src/types/elements.d.ts +10 -0
package/src/nodes.js
CHANGED
|
@@ -109,7 +109,12 @@ import {
|
|
|
109
109
|
windowOrigin,
|
|
110
110
|
} from './anchor.js';
|
|
111
111
|
import { baseTheme } from './palette.js';
|
|
112
|
-
import {
|
|
112
|
+
import {
|
|
113
|
+
callHandler,
|
|
114
|
+
ownerName,
|
|
115
|
+
reportStyleError,
|
|
116
|
+
STRICT_TOKENS,
|
|
117
|
+
} from './errors.js';
|
|
113
118
|
import {
|
|
114
119
|
hooks as a11yHooks,
|
|
115
120
|
isFocusable as a11yFocusable,
|
|
@@ -1542,6 +1547,13 @@ export class Node {
|
|
|
1542
1547
|
// delete themselves as they land; a loop entry (`loop: true`) is removed
|
|
1543
1548
|
// by `_updateLoops` and by nothing else
|
|
1544
1549
|
this._anim = null;
|
|
1550
|
+
// False until the first frame places this node (`absolutize`). Read by
|
|
1551
|
+
// `_retarget`: a style can be re-resolved several times between
|
|
1552
|
+
// construction and that first frame — the attach-time theme merge is the
|
|
1553
|
+
// common one, replacing a detached resolution against the desktop
|
|
1554
|
+
// palette with one against the app's own — and none of those is a
|
|
1555
|
+
// *change* the user saw, so no transition may start from it.
|
|
1556
|
+
this._placed = false;
|
|
1545
1557
|
// the loops this node's style declares, whether or not they are running
|
|
1546
1558
|
this._loops = null;
|
|
1547
1559
|
// `resolvedTextStyle()`'s cache: this node's own text style over what it
|
|
@@ -1557,6 +1569,11 @@ export class Node {
|
|
|
1557
1569
|
// subtree's hit reach, invalidated through _clearHitBounds()
|
|
1558
1570
|
this._paintOrderCache = null;
|
|
1559
1571
|
this._hitBoundsCache = null;
|
|
1572
|
+
// a `$token` the theme does not define, held for `commitMount` to throw
|
|
1573
|
+
// on this node's own fiber — see `_tokenProblem`. Strict mode only.
|
|
1574
|
+
// `null` is "commitMount is still to come", `false` is "it has been and
|
|
1575
|
+
// gone", and an Error is one waiting for it
|
|
1576
|
+
this._tokenError = null;
|
|
1560
1577
|
this._syncStyle(props);
|
|
1561
1578
|
this.yoga = yoga ? createLayoutNode() : null;
|
|
1562
1579
|
if (this.yoga) {
|
|
@@ -1588,7 +1605,7 @@ export class Node {
|
|
|
1588
1605
|
* them. `baseStyle` is the flattened `style` prop; `style` is that with
|
|
1589
1606
|
* the active state blocks overlaid.
|
|
1590
1607
|
*/
|
|
1591
|
-
_syncStyle(props) {
|
|
1608
|
+
_syncStyle(props, mounting = false) {
|
|
1592
1609
|
if (DEV && this.stylable) {
|
|
1593
1610
|
assertNoFlatStyleProps(props, this.kind, this.semanticNames);
|
|
1594
1611
|
validateStyle(flattenStyle(props.style), `<${this.kind} style>`);
|
|
@@ -1597,12 +1614,16 @@ export class Node {
|
|
|
1597
1614
|
this._usesTokens = this.stylable && styleUsesTokens(this._baseStyle);
|
|
1598
1615
|
if (this._usesTokens) {
|
|
1599
1616
|
const theme = this.theme;
|
|
1617
|
+
const strict = this.placed;
|
|
1618
|
+
const problems = strict ? [] : null;
|
|
1600
1619
|
this._baseStyle = resolveTokens(
|
|
1601
1620
|
this._baseStyle,
|
|
1602
1621
|
theme,
|
|
1603
1622
|
`<${this.kind} style>`,
|
|
1604
|
-
|
|
1623
|
+
strict,
|
|
1624
|
+
problems,
|
|
1605
1625
|
);
|
|
1626
|
+
if (problems?.length) this._tokenProblem(problems, mounting);
|
|
1606
1627
|
}
|
|
1607
1628
|
// `disabled` is a prop, not something the pointer does, so it is read
|
|
1608
1629
|
// straight off props rather than driven by the event manager
|
|
@@ -1687,24 +1708,32 @@ export class Node {
|
|
|
1687
1708
|
}
|
|
1688
1709
|
return this.style;
|
|
1689
1710
|
}
|
|
1690
|
-
for
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
(
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1711
|
+
// Only for a node the user has seen (`_placed`): between construction
|
|
1712
|
+
// and the first frame a style is re-resolved several times — attach
|
|
1713
|
+
// merges the real theme over the detached resolution's desktop palette,
|
|
1714
|
+
// queries settle — and animating any of those would travel from a value
|
|
1715
|
+
// that was never on screen. An inserted element *appears* at its style;
|
|
1716
|
+
// transitions start on later changes, which is CSS's rule too.
|
|
1717
|
+
if (this._placed) {
|
|
1718
|
+
for (const prop of Object.keys(target)) {
|
|
1719
|
+
const to = target[prop];
|
|
1720
|
+
const from = displayed[prop];
|
|
1721
|
+
if (from === to || from === undefined) continue;
|
|
1722
|
+
const duration = transitionFor(target, prop);
|
|
1723
|
+
if (duration <= 0) continue;
|
|
1724
|
+
if (interpolate(from, to, 0.5) === null) continue; // no midpoint: snap
|
|
1725
|
+
(this._anim ??= new Map()).set(prop, {
|
|
1726
|
+
from,
|
|
1727
|
+
to,
|
|
1728
|
+
duration,
|
|
1729
|
+
// *now*, not the last frame's timestamp: between two user actions
|
|
1730
|
+
// the window is idle and draws nothing, so the previous frame can
|
|
1731
|
+
// be seconds old — and the first tick would then find the
|
|
1732
|
+
// transition already over and jump straight to the end
|
|
1733
|
+
start: now(),
|
|
1734
|
+
});
|
|
1735
|
+
this.root?._startAnimating(this);
|
|
1736
|
+
}
|
|
1708
1737
|
}
|
|
1709
1738
|
// After the transitions, before the style is assembled: a loop that just
|
|
1710
1739
|
// arrived contributes a value to this very swap, so the first frame the
|
|
@@ -2241,6 +2270,46 @@ export class Node {
|
|
|
2241
2270
|
return owner.isPopup ? owner.parent != null : true;
|
|
2242
2271
|
}
|
|
2243
2272
|
|
|
2273
|
+
/**
|
|
2274
|
+
* A `$token` this node's completed ancestry does not define.
|
|
2275
|
+
*
|
|
2276
|
+
* The default is `reportStyleError`: say so loudly, set `process.exitCode`,
|
|
2277
|
+
* and keep the property dropped. `REACT_X11_STRICT_TOKENS=1` makes it fatal
|
|
2278
|
+
* again, and then *where* the throw lands is the whole question — an error
|
|
2279
|
+
* boundary only catches what React invoked, on the fiber React thinks it
|
|
2280
|
+
* is working on.
|
|
2281
|
+
*
|
|
2282
|
+
* `mounting` is the attach walk, which runs inside `appendInitialChild`
|
|
2283
|
+
* while React is completing the nearest host *ancestor* — the `<window>`,
|
|
2284
|
+
* for a whole tree rendered at once. A throw there is attributed to the
|
|
2285
|
+
* window and sails past every boundary the app wrote inside it, which is
|
|
2286
|
+
* the bug this deferral exists for (#420). Stashed instead, and thrown
|
|
2287
|
+
* from `commitMount` on this node's own fiber, where the walk up finds a
|
|
2288
|
+
* boundary at any depth.
|
|
2289
|
+
*
|
|
2290
|
+
* Every other caller already has the right fiber (`commitUpdate`) or has
|
|
2291
|
+
* no React on the stack at all (`appearanceChanged`, from an X event) —
|
|
2292
|
+
* for those, throwing here is both the earliest and the only option, and
|
|
2293
|
+
* the second is the crash strict mode asked for.
|
|
2294
|
+
*
|
|
2295
|
+
* `commitMount` happens once per instance, so a node re-attached after it
|
|
2296
|
+
* has been and gone has nothing left to defer *to*; stashing there would
|
|
2297
|
+
* swallow the error instead of raising it late. Those throw at once, like
|
|
2298
|
+
* the keyed reorder they resemble.
|
|
2299
|
+
*/
|
|
2300
|
+
_tokenProblem(problems, mounting) {
|
|
2301
|
+
if (!STRICT_TOKENS) {
|
|
2302
|
+
// every one of them: two misspellings in a style are two things to
|
|
2303
|
+
// fix, and a report that named only the first would send someone back
|
|
2304
|
+
// for a second run to find the second
|
|
2305
|
+
for (const message of problems) reportStyleError(this, message);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const error = new Error(problems[0]);
|
|
2309
|
+
if (mounting && this._tokenError === null) this._tokenError = error;
|
|
2310
|
+
else throw error;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2244
2313
|
/** The owning window resized: re-resolve, since a query block may now
|
|
2245
2314
|
* match that did not, or the other way round. */
|
|
2246
2315
|
_sizeQueriesChanged() {
|
|
@@ -2287,7 +2356,7 @@ export class Node {
|
|
|
2287
2356
|
if (this.isWindow) this._syncWindowBackground();
|
|
2288
2357
|
if (this._usesTokens) {
|
|
2289
2358
|
const before = this.style;
|
|
2290
|
-
this._syncStyle(this.props);
|
|
2359
|
+
this._syncStyle(this.props, mounting);
|
|
2291
2360
|
// a token change reaches the node without React re-rendering it, so
|
|
2292
2361
|
// the invalidation a commit would have done has to happen here too
|
|
2293
2362
|
if (localTextStyleChanged(this.style, before)) {
|
|
@@ -3199,6 +3268,9 @@ export class Node {
|
|
|
3199
3268
|
}
|
|
3200
3269
|
|
|
3201
3270
|
absolutize(originX, originY) {
|
|
3271
|
+
// before the yoga check, so a span — placed by its paragraph, no box of
|
|
3272
|
+
// its own — counts as on screen too
|
|
3273
|
+
this._placed = true;
|
|
3202
3274
|
if (!this.yoga) return;
|
|
3203
3275
|
this._assignAbs(
|
|
3204
3276
|
originX + this.yoga.getComputedLeft(),
|
|
@@ -4122,19 +4194,28 @@ export class Node {
|
|
|
4122
4194
|
* One blurred shadow, through the paint cache when there is one.
|
|
4123
4195
|
*
|
|
4124
4196
|
* The surface is the shadow's rectangle plus `pad` on every side, and the
|
|
4125
|
-
* padding is load-bearing:
|
|
4197
|
+
* padding is load-bearing: a convolution reads outside the picture as
|
|
4126
4198
|
* transparent, so a kernel that runs off the edge ends the shadow in a
|
|
4127
|
-
* straight line.
|
|
4128
|
-
*
|
|
4129
|
-
*
|
|
4199
|
+
* straight line. `blurKernel` takes that reach from the same function
|
|
4200
|
+
* ntk builds the kernel with, so the two cannot drift apart.
|
|
4201
|
+
*
|
|
4202
|
+
* The blur is **baked into the pixels** by `blurCoverage` (ntk 8.6,
|
|
4203
|
+
* ntk#335) rather than set as a filter on the picture. That is the
|
|
4204
|
+
* difference between a cached shadow and a cached shadow that costs
|
|
4205
|
+
* nothing to draw: a picture's filter is re-applied by the server on every
|
|
4206
|
+
* composite, so the entry would hit, re-render nothing, and still pay its
|
|
4207
|
+
* whole kernel every frame — 244M multiply-accumulates for one card-sized
|
|
4208
|
+
* shadow, which was 1.6s per `:hover` on XQuartz. Baked, what the cache
|
|
4209
|
+
* holds composites as an ordinary mask however wide the blur was, and the
|
|
4210
|
+
* two separable passes run once per distinct geometry.
|
|
4130
4211
|
*
|
|
4131
4212
|
* `maxPixels` is raised well above the cache's default: a card's shadow is
|
|
4132
4213
|
* as big as the card, an entry for one is a8 (a byte a pixel), and the
|
|
4133
|
-
* thing being avoided
|
|
4134
|
-
*
|
|
4214
|
+
* thing being avoided is exactly the cost the default cap bounds
|
|
4215
|
+
* elsewhere.
|
|
4135
4216
|
*/
|
|
4136
4217
|
_paintBlurredShadow(ctx, rect, radius, blur, color) {
|
|
4137
|
-
const { sigma,
|
|
4218
|
+
const { sigma, pad } = blurKernel(blur);
|
|
4138
4219
|
// integral, because the surface is pixels; the blur is far wider than
|
|
4139
4220
|
// the rounding, so nothing about the result is visibly quantized
|
|
4140
4221
|
const width = Math.round(rect.width);
|
|
@@ -4148,6 +4229,10 @@ export class Node {
|
|
|
4148
4229
|
format: 'a8',
|
|
4149
4230
|
tint: color,
|
|
4150
4231
|
maxPixels: 1024 * 1024,
|
|
4232
|
+
// Cache on the first sighting rather than the second: what the gate
|
|
4233
|
+
// saves elsewhere is a cheap redraw, and what it costs here is a whole
|
|
4234
|
+
// gaussian — the one thing this entry exists to avoid running twice.
|
|
4235
|
+
eager: true,
|
|
4151
4236
|
draw: (sctx, box) => {
|
|
4152
4237
|
// full coverage: the colour arrives at composite time
|
|
4153
4238
|
sctx.fillStyle = '#ffffff';
|
|
@@ -4158,7 +4243,7 @@ export class Node {
|
|
|
4158
4243
|
);
|
|
4159
4244
|
sctx.fill();
|
|
4160
4245
|
},
|
|
4161
|
-
after: (surface) =>
|
|
4246
|
+
after: (surface) => ntk.blurCoverage(surface, sigma),
|
|
4162
4247
|
live: () => this._paintShadowLive(ctx, plan),
|
|
4163
4248
|
};
|
|
4164
4249
|
const cache = this.root?._paintCache;
|
|
@@ -4171,7 +4256,9 @@ export class Node {
|
|
|
4171
4256
|
* build, an entry too big for the budget, and the first frame of a shadow
|
|
4172
4257
|
* the cache has only seen once. A surface per frame is what a shadow costs
|
|
4173
4258
|
* without a cache; it is still one composite on the wire, and the
|
|
4174
|
-
* alternative is not painting it.
|
|
4259
|
+
* alternative is not painting it. The blur is baked here too: two
|
|
4260
|
+
* separable passes and a plain composite still beat one composite through
|
|
4261
|
+
* a k x k kernel, by the ratio of 2k to k squared.
|
|
4175
4262
|
*/
|
|
4176
4263
|
_paintShadowLive(ctx, plan) {
|
|
4177
4264
|
if (typeof ntk.Surface !== 'function' || !this.app?.display?.Render) return;
|
|
@@ -4185,7 +4272,10 @@ export class Node {
|
|
|
4185
4272
|
surface.render((sctx) =>
|
|
4186
4273
|
plan.draw(sctx, { x: 0, y: 0, width: plan.width, height: plan.height }),
|
|
4187
4274
|
);
|
|
4188
|
-
|
|
4275
|
+
// `after` may hand back a *different* surface — the blur is baked into
|
|
4276
|
+
// a second one and the sharp copy destroyed — so both the drawing and
|
|
4277
|
+
// the cleanup below follow what it returned.
|
|
4278
|
+
surface = plan.after(surface) ?? surface;
|
|
4189
4279
|
const before = ctx.fillStyle;
|
|
4190
4280
|
ctx.fillStyle = plan.tint;
|
|
4191
4281
|
ctx.drawImage(surface, plan.x, plan.y);
|
|
@@ -5425,6 +5515,7 @@ export const Scrollable = (Base) =>
|
|
|
5425
5515
|
}
|
|
5426
5516
|
|
|
5427
5517
|
absolutize(originX, originY) {
|
|
5518
|
+
this._placed = true;
|
|
5428
5519
|
if (!this.yoga) return;
|
|
5429
5520
|
this._assignAbs(
|
|
5430
5521
|
originX + this.yoga.getComputedLeft(),
|
|
@@ -6476,13 +6567,16 @@ export function openEditMenu(node, at, actions = {}) {
|
|
|
6476
6567
|
if (items.length === 0) return;
|
|
6477
6568
|
|
|
6478
6569
|
const style = node.resolvedTextStyle();
|
|
6570
|
+
// `at` is `{x: ev.x, y: ev.y}` per the doc above — logical, like every
|
|
6571
|
+
// coordinate a handler reads — and everything below is device: the
|
|
6572
|
+
// geometry takes the scale so its chrome lands on the same grid as the
|
|
6573
|
+
// device-sized text it measures.
|
|
6574
|
+
const s = node.scale;
|
|
6479
6575
|
const geometry = editMenuGeometry(
|
|
6480
6576
|
items,
|
|
6481
6577
|
(text) => app?.fonts?.layout(text, style)?.width,
|
|
6578
|
+
s,
|
|
6482
6579
|
);
|
|
6483
|
-
// `at` is `{x: ev.x, y: ev.y}` per the doc above — logical, like every
|
|
6484
|
-
// coordinate a handler reads — and the origin math below is device.
|
|
6485
|
-
const s = node.scale;
|
|
6486
6580
|
const deviceAt = at && {
|
|
6487
6581
|
...at,
|
|
6488
6582
|
...(Number.isFinite(at.x) && { x: at.x * s }),
|
|
@@ -10173,8 +10267,20 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10173
10267
|
* before laying out. This is the whole reason a size query may carry
|
|
10174
10268
|
* layout properties while a state block may not: it only ever runs inside
|
|
10175
10269
|
* a layout pass the resize already required.
|
|
10176
|
-
|
|
10177
|
-
|
|
10270
|
+
*
|
|
10271
|
+
* Callers pass the size in device pixels — it comes off the window or out
|
|
10272
|
+
* of yoga, and both live on the device grid — but `querySize` is stored
|
|
10273
|
+
* in **logical** pixels, because that is the unit the thresholds were
|
|
10274
|
+
* written in: `'@width >= 620'` sits in a style block next to `width:
|
|
10275
|
+
* 620`, and the same number must mean the same thing. At scale 1 the two
|
|
10276
|
+
* coincide, which is how comparing device pixels survived every 1x
|
|
10277
|
+
* display it was ever run on and broke on the first retina one (every
|
|
10278
|
+
* query read double, so none of them ever changed answer under a drag).
|
|
10279
|
+
*/
|
|
10280
|
+
_resolveSizeQueries(deviceWidth, deviceHeight) {
|
|
10281
|
+
const s = this.scale || 1;
|
|
10282
|
+
const width = deviceWidth / s;
|
|
10283
|
+
const height = deviceHeight / s;
|
|
10178
10284
|
if (this._sizeQueryNodes.size === 0) {
|
|
10179
10285
|
this.querySize = this.querySize ?? { width, height };
|
|
10180
10286
|
return false;
|
|
@@ -10398,6 +10504,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10398
10504
|
}
|
|
10399
10505
|
(this._frameReasons ??= new Set()).add(reason);
|
|
10400
10506
|
}
|
|
10507
|
+
// A retained presenter keeps a per-node diff instead of damage rects,
|
|
10508
|
+
// and this is the one channel every change already announces itself on
|
|
10509
|
+
// (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
|
|
10510
|
+
// ntk window has no ear here and the X11 path is byte-identical.
|
|
10511
|
+
this.window?.noteInvalidate?.(damage, layoutChanged, reason);
|
|
10401
10512
|
if (layoutChanged) {
|
|
10402
10513
|
this.needsLayout = true;
|
|
10403
10514
|
// The content floors are measured from the tree, so anything that
|
|
@@ -10562,6 +10673,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10562
10673
|
this.yoga.setHeight(height);
|
|
10563
10674
|
this.yoga.calculateLayout(width, height, this._rootDirection);
|
|
10564
10675
|
this.abs = { x: 0, y: 0, width, height };
|
|
10676
|
+
this._placed = true;
|
|
10565
10677
|
// the root's rect is written here, not through _assignAbs, so its
|
|
10566
10678
|
// cached hit reach is dropped here too (children bubble their own)
|
|
10567
10679
|
this._hitBoundsCache = null;
|
|
@@ -10637,6 +10749,16 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10637
10749
|
);
|
|
10638
10750
|
}
|
|
10639
10751
|
this._fullRepaintCause = null;
|
|
10752
|
+
// A retained presenter takes the frame from here: the model half above —
|
|
10753
|
+
// animations, layout, absolutize, the scroll offsets — is shared, and
|
|
10754
|
+
// what changes per backend is how a frame reaches the screen. The damage
|
|
10755
|
+
// list was still taken (its bookkeeping is what keeps the two paths one
|
|
10756
|
+
// code) and is simply not consumed; the presenter diffs at the layer.
|
|
10757
|
+
if (typeof this.window.presentFrame === 'function') {
|
|
10758
|
+
this.window.presentFrame(this, damage);
|
|
10759
|
+
this.app._reactX11Startup?.painted();
|
|
10760
|
+
return;
|
|
10761
|
+
}
|
|
10640
10762
|
if (typeof this.window.getContext !== 'function') return; // headless mock
|
|
10641
10763
|
// ntk getContext creates a fresh context (with window-event
|
|
10642
10764
|
// subscriptions) on every call — cache one per window
|
|
@@ -10657,6 +10779,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10657
10779
|
// after every region: an entry drawn in one damage rect must not be
|
|
10658
10780
|
// evicted before the next rect of the same frame asks for it
|
|
10659
10781
|
this._paintCache?.endFrame();
|
|
10782
|
+
// The swapchain seam: a backend presenting from double buffers has to
|
|
10783
|
+
// know exactly which pixels each flush touched — several flushes can
|
|
10784
|
+
// land between two presents, so reading only the last frame's rects
|
|
10785
|
+
// would leave the flipped-in back buffer stale where an earlier flush
|
|
10786
|
+
// painted. Feature-detected like presentFrame; null means everything.
|
|
10787
|
+
this.window.noteFrameDamage?.(damage ?? null);
|
|
10660
10788
|
if (frameHook) {
|
|
10661
10789
|
frameHook({
|
|
10662
10790
|
root: this,
|
package/src/paintcache.js
CHANGED
|
@@ -232,7 +232,13 @@ export class PaintCache {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
this.stats.misses++;
|
|
235
|
-
|
|
235
|
+
// `eager` skips the gate below for a drawing whose *live* path is the
|
|
236
|
+
// expensive thing — a blurred shadow, whose first sighting otherwise
|
|
237
|
+
// runs a convolution that is then thrown away, and a second one to keep.
|
|
238
|
+
// The gate is there so a page cycling unique content cannot fill the
|
|
239
|
+
// cache with entries drawn once; an eager plan opts out of that
|
|
240
|
+
// protection deliberately, and the LRU budget is what still bounds it.
|
|
241
|
+
const seen = plan.eager ? 2 : (this.pending.get(plan.key) ?? 0) + 1;
|
|
236
242
|
if (seen < 2) {
|
|
237
243
|
if (this.pending.size >= MAX_PENDING) this.pending.clear();
|
|
238
244
|
this.pending.set(plan.key, seen);
|
|
@@ -260,12 +266,17 @@ export class PaintCache {
|
|
|
260
266
|
surface.render((sctx) =>
|
|
261
267
|
plan.draw(this.verify ? recordingContext(sctx, state) : sctx, box),
|
|
262
268
|
);
|
|
263
|
-
|
|
269
|
+
// `after` may hand back a *different* surface than it was given — a
|
|
270
|
+
// blurred shadow bakes its convolution into a second one and destroys
|
|
271
|
+
// the sharp copy, so that what this entry holds composites as a plain
|
|
272
|
+
// mask instead of re-running a kernel on every blit. What it returns
|
|
273
|
+
// is what the cache owns from here on.
|
|
274
|
+
const final = plan.after?.(surface) ?? surface;
|
|
264
275
|
this.stats.renders++;
|
|
265
276
|
return {
|
|
266
277
|
key: plan.key,
|
|
267
|
-
surface,
|
|
268
|
-
bytes:
|
|
278
|
+
surface: final,
|
|
279
|
+
bytes: final.bytes,
|
|
269
280
|
digest: this.verify ? state.digest : 0,
|
|
270
281
|
};
|
|
271
282
|
} catch (err) {
|
package/src/palette.js
CHANGED
|
@@ -222,6 +222,19 @@ export const DefaultTheme = {
|
|
|
222
222
|
// than it looks next to a CSS padding for that reason: 12 here is about
|
|
223
223
|
// what 8 came to once a typical face's ascent had been added on.
|
|
224
224
|
paddingY: 12,
|
|
225
|
+
// Which scheme this palette *is* — 'light' or 'dark'. Not a colour but a
|
|
226
|
+
// fact about the colours, for the consumers that have to match them with
|
|
227
|
+
// something they do not paint themselves: the Cocoa backend picks the
|
|
228
|
+
// AppKit appearance its native control bezels are rendered in from this,
|
|
229
|
+
// so a pinned-light app gets light bezels on a dark desktop. A custom
|
|
230
|
+
// dark palette built over the light base should say `scheme: 'dark'`.
|
|
231
|
+
scheme: 'light',
|
|
232
|
+
// How the core controls render where the backend offers the platform's
|
|
233
|
+
// own: `'auto'` (native where supported — today the Cocoa backend),
|
|
234
|
+
// `'native'` (ask for it; warns and falls back to drawn where there is
|
|
235
|
+
// none) or `'drawn'` (always the themed rendering). Per-instance escape
|
|
236
|
+
// hatch: `native={false}` on the one custom-branded control.
|
|
237
|
+
controls: 'auto',
|
|
225
238
|
};
|
|
226
239
|
|
|
227
240
|
// Which pressed token is derived from which pair, when the palette does not
|
|
@@ -332,6 +345,7 @@ export function resolveTheme(value, base = DefaultTheme) {
|
|
|
332
345
|
* theme after it gets to do.
|
|
333
346
|
*/
|
|
334
347
|
export const DarkTheme = resolveTheme({
|
|
348
|
+
scheme: 'dark',
|
|
335
349
|
// A near-black with a little blue in it rather than #000: pure black shows
|
|
336
350
|
// every seam between a window and the widgets on it, and no desktop's dark
|
|
337
351
|
// theme uses it.
|
package/src/style.d.ts
CHANGED
|
@@ -43,12 +43,15 @@ export function tokenNames(
|
|
|
43
43
|
style: StyleProperties,
|
|
44
44
|
out?: Set<string>,
|
|
45
45
|
): Set<string>;
|
|
46
|
-
/** Replace `$token` references with values from the theme.
|
|
46
|
+
/** Replace `$token` references with values from the theme. A token the theme
|
|
47
|
+
* does not define is dropped either way; with `strict`, the message naming
|
|
48
|
+
* it is pushed onto `problems` for the caller to report or throw. */
|
|
47
49
|
export function resolveTokens(
|
|
48
50
|
style: StyleProperties,
|
|
49
51
|
theme: Record<string, unknown> | null | undefined,
|
|
50
52
|
where?: string,
|
|
51
53
|
strict?: boolean,
|
|
54
|
+
problems?: string[] | null,
|
|
52
55
|
): StyleProperties;
|
|
53
56
|
|
|
54
57
|
export function styleHasSizeQueries(style: StyleProperties): boolean;
|
package/src/styles.js
CHANGED
|
@@ -466,6 +466,11 @@ const isState = (key) => key.charCodeAt(0) === 58; /* ':' */
|
|
|
466
466
|
* what a style can usefully ask about here is the window it is being laid
|
|
467
467
|
* out in, not the screen.
|
|
468
468
|
*
|
|
469
|
+
* The threshold is **logical** pixels, like every other number in a style
|
|
470
|
+
* block: `'@width >= 620'` flips where `width: 620` would fit, whatever
|
|
471
|
+
* the display scale (the window node divides its device size out before
|
|
472
|
+
* matching — `_resolveSizeQueries`).
|
|
473
|
+
*
|
|
469
474
|
* Unlike a state block, a size query *may* set layout properties. That is
|
|
470
475
|
* not an inconsistency: pointer state changes must never reflow the tree,
|
|
471
476
|
* but a size query is only ever re-evaluated during a layout pass that a
|
|
@@ -1206,16 +1211,37 @@ export function stripTokens(style) {
|
|
|
1206
1211
|
* `strict` says the node's ancestry is complete, so a token that does not
|
|
1207
1212
|
* resolve is a mistake. While a subtree is still being built its nodes can
|
|
1208
1213
|
* see only part of their ancestry — the theme two levels up does not exist
|
|
1209
|
-
* for them yet — so resolution there is provisional
|
|
1210
|
-
*
|
|
1214
|
+
* for them yet — so resolution there is provisional.
|
|
1215
|
+
*
|
|
1216
|
+
* Both cases drop the property, because a value is either resolved or absent
|
|
1217
|
+
* and `'$textMuted1'` is not a colour. What `strict` changes is whether
|
|
1218
|
+
* anyone hears about it: mistakes are pushed onto `problems` and the caller
|
|
1219
|
+
* decides what one costs. Resolving itself never throws — it runs from a
|
|
1220
|
+
* commit and from an X event alike, and only the caller knows whether React
|
|
1221
|
+
* is on the stack to route a throw to a boundary (src/nodes.js).
|
|
1222
|
+
*
|
|
1223
|
+
* A cache hit replays the problems it recorded, so the second node to wear a
|
|
1224
|
+
* misspelled shared style is reported like the first.
|
|
1211
1225
|
*/
|
|
1212
|
-
export function resolveTokens(
|
|
1226
|
+
export function resolveTokens(
|
|
1227
|
+
style,
|
|
1228
|
+
theme,
|
|
1229
|
+
where = 'style',
|
|
1230
|
+
strict = true,
|
|
1231
|
+
problems = null,
|
|
1232
|
+
) {
|
|
1213
1233
|
if (!theme) return stripTokens(style);
|
|
1214
1234
|
let byTheme = strict ? resolvedCache.get(style) : null;
|
|
1215
1235
|
if (strict && !byTheme) resolvedCache.set(style, (byTheme = new WeakMap()));
|
|
1216
1236
|
const hit = byTheme?.get(theme);
|
|
1217
|
-
if (hit)
|
|
1237
|
+
if (hit) {
|
|
1238
|
+
if (problems && hit.problems) problems.push(...hit.problems);
|
|
1239
|
+
return hit.out;
|
|
1240
|
+
}
|
|
1218
1241
|
|
|
1242
|
+
// collected here rather than pushed straight to `problems` so the cache
|
|
1243
|
+
// entry can keep them: the caller that misses is not the only one to hear
|
|
1244
|
+
const found = strict ? [] : null;
|
|
1219
1245
|
const out = {};
|
|
1220
1246
|
for (const key of Object.keys(style)) {
|
|
1221
1247
|
const v = style[key];
|
|
@@ -1225,11 +1251,7 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1225
1251
|
out[key] = theme[name];
|
|
1226
1252
|
continue;
|
|
1227
1253
|
}
|
|
1228
|
-
if (
|
|
1229
|
-
throw new Error(
|
|
1230
|
-
`react-x11: unknown theme token "${v}" in ${where} ` +
|
|
1231
|
-
`(theme has ${Object.keys(theme).join(', ') || 'nothing'})`,
|
|
1232
|
-
);
|
|
1254
|
+
if (strict) found.push(unknownToken(v, theme, where));
|
|
1233
1255
|
} else if (mentionsToken(v)) {
|
|
1234
1256
|
let unknown = null;
|
|
1235
1257
|
const substituted = v.replace(TOKEN_IN_VALUE, (token) => {
|
|
@@ -1245,13 +1267,10 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1245
1267
|
// frame instead of at the style.
|
|
1246
1268
|
if (!unknown) out[key] = substituted;
|
|
1247
1269
|
else if (strict) {
|
|
1248
|
-
|
|
1249
|
-
`react-x11: unknown theme token "${unknown}" in ${where} ${key} ` +
|
|
1250
|
-
`(theme has ${Object.keys(theme).join(', ') || 'nothing'})`,
|
|
1251
|
-
);
|
|
1270
|
+
found.push(unknownToken(unknown, theme, `${where} ${key}`));
|
|
1252
1271
|
}
|
|
1253
1272
|
} else if (key.charCodeAt(0) === 58 && v) {
|
|
1254
|
-
out[key] = resolveTokens(v, theme, `${where} ${key}`, strict);
|
|
1273
|
+
out[key] = resolveTokens(v, theme, `${where} ${key}`, strict, found);
|
|
1255
1274
|
} else if (key === 'animation' && v && typeof v === 'object') {
|
|
1256
1275
|
const loops = {};
|
|
1257
1276
|
let incomplete = false;
|
|
@@ -1266,11 +1285,11 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1266
1285
|
theme,
|
|
1267
1286
|
`${where} animation ${prop}`,
|
|
1268
1287
|
strict,
|
|
1288
|
+
found,
|
|
1269
1289
|
);
|
|
1270
|
-
// A
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
// the ancestry to complete rather than throwing at a half of one.
|
|
1290
|
+
// A loop with one end missing is not a shorter loop, so a
|
|
1291
|
+
// declaration that lost a value is dropped whole rather than run
|
|
1292
|
+
// between a colour and nothing.
|
|
1274
1293
|
if (Object.keys(resolved).length !== Object.keys(entry).length) {
|
|
1275
1294
|
incomplete = true;
|
|
1276
1295
|
}
|
|
@@ -1281,10 +1300,20 @@ export function resolveTokens(style, theme, where = 'style', strict = true) {
|
|
|
1281
1300
|
out[key] = v;
|
|
1282
1301
|
}
|
|
1283
1302
|
}
|
|
1284
|
-
|
|
1303
|
+
if (found?.length && problems) problems.push(...found);
|
|
1304
|
+
byTheme?.set(theme, { out, problems: found?.length ? found : null });
|
|
1285
1305
|
return out;
|
|
1286
1306
|
}
|
|
1287
1307
|
|
|
1308
|
+
/** The one message, written once: what was named, and what the palette in
|
|
1309
|
+
* force actually has — listing the alternatives is most of the fix. */
|
|
1310
|
+
function unknownToken(token, theme, where) {
|
|
1311
|
+
return (
|
|
1312
|
+
`react-x11: unknown theme token "${token}" in ${where} ` +
|
|
1313
|
+
`(theme has ${Object.keys(theme).join(', ') || 'nothing'})`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1288
1317
|
export { validateStyle };
|
|
1289
1318
|
|
|
1290
1319
|
/**
|
package/src/types/elements.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
SubmitEvent,
|
|
24
24
|
SyntheticEvent,
|
|
25
25
|
ViewportEvent,
|
|
26
|
+
WheelEvent,
|
|
26
27
|
WindowResizeEvent,
|
|
27
28
|
} from './events.js';
|
|
28
29
|
|
|
@@ -841,6 +842,15 @@ export interface GlAreaProps extends DrawnProps<DrawnNode> {
|
|
|
841
842
|
onDraw?: (gl: any, info: DrawInfo) => void;
|
|
842
843
|
/** No GL surface — no GLX, or no matching visual. */
|
|
843
844
|
onError?: (err: Error) => void;
|
|
845
|
+
/**
|
|
846
|
+
* The wheel over the surface. Inherited from `EventHandlers` like every
|
|
847
|
+
* other element's, and listed here because it is the **only** pointer
|
|
848
|
+
* event a `<glarea>` currently reports: the surface owns its own X window,
|
|
849
|
+
* so it selects the wheel there and hands it to the window's event manager
|
|
850
|
+
* (see docs/elements.md). Deltas are pixels, `preventDefault()` takes the
|
|
851
|
+
* default scroll action back, and it bubbles from this node.
|
|
852
|
+
*/
|
|
853
|
+
onWheel?: (ev: WheelEvent<DrawnNode>) => void;
|
|
844
854
|
/** A click inside the surface that hit no mesh. */
|
|
845
855
|
onPointerMissed?: (ev: MouseEvent<DrawnNode>) => void;
|
|
846
856
|
}
|