react-x11 2.27.0 → 2.29.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/package.json +2 -2
- package/src/cocoa/presenter.js +8 -3
- package/src/cocoa/promotion.js +107 -30
- package/src/cocoa/sprites.js +562 -0
- package/src/cocoa/window.js +4 -0
- package/src/node.d.ts +91 -0
- package/src/nodes/node.js +8 -1
- package/src/nodes/scrollable.js +81 -43
- package/src/nodes/sprites.js +73 -0
- package/src/nodes/window/window.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"yoga-layout": "^3.2.1"
|
|
109
109
|
},
|
|
110
110
|
"optionalDependencies": {
|
|
111
|
-
"@windowkit/appkit": "^0.
|
|
111
|
+
"@windowkit/appkit": "^0.19.0",
|
|
112
112
|
"@windowkit/wayland": "^3.1.1",
|
|
113
113
|
"@windowkit/win32": "^0.0.6",
|
|
114
114
|
"dbus-native": "^0.16.0",
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -356,6 +356,9 @@ const ANIMATED_KEY_PATHS = Object.freeze({
|
|
|
356
356
|
borderColor: { keyPath: 'borderColor', colour: true },
|
|
357
357
|
borderWidth: { keyPath: 'borderWidth', scaled: true },
|
|
358
358
|
borderRadius: { keyPath: 'cornerRadius', scaled: true },
|
|
359
|
+
// the subtree's, as CSS has it: a layer under an opacity is drawn with
|
|
360
|
+
// its sublayers and faded as one (`layerOpacity`)
|
|
361
|
+
opacity: { keyPath: 'opacity' },
|
|
359
362
|
});
|
|
360
363
|
|
|
361
364
|
// the ids the bridge reports an animation's end under: unique per process,
|
|
@@ -487,9 +490,11 @@ export function propBoxProps(node, app, scale, parentOrigin, order) {
|
|
|
487
490
|
}
|
|
488
491
|
|
|
489
492
|
/**
|
|
490
|
-
* `opacity` as a layer's own.
|
|
491
|
-
* its
|
|
492
|
-
*
|
|
493
|
+
* `opacity` as a layer's own. A node's children are its layer's sublayers —
|
|
494
|
+
* each its own on the layer presenter, one raster on a promoted box — and
|
|
495
|
+
* Core Animation draws a layer under an opacity below 1 with its sublayers
|
|
496
|
+
* and fades them as one (`allowsGroupOpacity`, YES by default on macOS): the
|
|
497
|
+
* group `NodePaint._paintGroup` draws on the bitmap.
|
|
493
498
|
*/
|
|
494
499
|
export function layerOpacity(style) {
|
|
495
500
|
const value = style?.opacity;
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
// promoted only when nothing painted after it in the walk reaches into its
|
|
25
25
|
// bounds — no later sibling at any level, no ancestor's border ring or
|
|
26
26
|
// focus ring, no scrollbar — and only when every clipping ancestor holds
|
|
27
|
-
// the whole of it, because the layer would not be clipped
|
|
27
|
+
// the whole of it, because the layer would not be clipped, and no ancestor
|
|
28
|
+
// fades, because the layer would not be faded with it. Declining is
|
|
28
29
|
// always safe: the frame clock runs the animation exactly as it does
|
|
29
30
|
// without this file. And the same test runs again every frame, so a node
|
|
30
|
-
// that becomes overlapped, hidden, clipped or non-plain returns to
|
|
31
|
-
// bitmap in the frame that finds it, the animation handed back to the
|
|
31
|
+
// that becomes overlapped, hidden, clipped, faded or non-plain returns to
|
|
32
|
+
// the bitmap in the frame that finds it, the animation handed back to the
|
|
32
33
|
// clock. Overlays, toasts, drag ghosts, spinners and floating cards pass by
|
|
33
34
|
// construction; a hover fade on a row in the middle of a list does not,
|
|
34
35
|
// and stays on the clock. docs/macos.md §"Layer promotion" is the account.
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
propBoxProps,
|
|
45
46
|
stylePaintsPlain,
|
|
46
47
|
} from './presenter.js';
|
|
48
|
+
import { SpriteLayers } from './sprites.js';
|
|
47
49
|
|
|
48
50
|
const ORIGIN = Object.freeze({ x: 0, y: 0 });
|
|
49
51
|
|
|
@@ -60,6 +62,17 @@ const IDLE_GRACE_MS = 1000;
|
|
|
60
62
|
// the repaint they save (the layer presenter's rule, per raster).
|
|
61
63
|
const MAX_DIRTY_RECTS = 16;
|
|
62
64
|
|
|
65
|
+
/** Paint order, for entries carrying a `_paintKey`: a key the walk would
|
|
66
|
+
* not reach sorts last. */
|
|
67
|
+
function byPaintKey(a, b) {
|
|
68
|
+
if (!a.key || !b.key) return (a.key ? 1 : 0) - (b.key ? 1 : 0);
|
|
69
|
+
const n = Math.min(a.key.length, b.key.length);
|
|
70
|
+
for (let i = 0; i < n; i++) {
|
|
71
|
+
if (a.key[i] !== b.key[i]) return a.key[i] - b.key[i];
|
|
72
|
+
}
|
|
73
|
+
return a.key.length - b.key.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
const rectsOverlap = (a, b) =>
|
|
64
77
|
a.x < b.x + b.width &&
|
|
65
78
|
b.x < a.x + a.width &&
|
|
@@ -157,12 +170,14 @@ function insideGlArea(node) {
|
|
|
157
170
|
const faded = (style) => style?.opacity !== undefined && !(style.opacity >= 1);
|
|
158
171
|
|
|
159
172
|
/**
|
|
160
|
-
* A box whose `opacity` is below 1, now or on its way there, is a group
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* layer
|
|
173
|
+
* A box whose `opacity` is below 1, now or on its way there, is a group:
|
|
174
|
+
* the box and everything in it drawn once and faded as one. The bitmap
|
|
175
|
+
* composites one (`NodePaint._paintGroup`), and so does Core Animation — a
|
|
176
|
+
* layer under an opacity below 1 is drawn with its sublayers and faded as
|
|
177
|
+
* one (`allowsGroupOpacity`, YES by default on macOS), and a promoted box's
|
|
178
|
+
* children are a sublayer of its layer. So a faded box may be promoted;
|
|
179
|
+
* what may not is a box inside one, whose layer would be flat on the window
|
|
180
|
+
* root, outside the group and at full strength (`_clear`).
|
|
166
181
|
*/
|
|
167
182
|
function fadesAsGroup(node) {
|
|
168
183
|
return (
|
|
@@ -182,7 +197,6 @@ function fadesAsGroup(node) {
|
|
|
182
197
|
function promotableNode(node) {
|
|
183
198
|
if (node.destroyed || !plainBox(node)) return false;
|
|
184
199
|
if (insideGlArea(node)) return false;
|
|
185
|
-
if (fadesAsGroup(node)) return false;
|
|
186
200
|
if (!stylePaintsPlain(node, node._targetStyle ?? node.style)) return false;
|
|
187
201
|
if (node.isScroller?.()) return false;
|
|
188
202
|
return !paintsOutline(node);
|
|
@@ -241,6 +255,9 @@ export class CocoaPromotion {
|
|
|
241
255
|
layerOf: (node) => this.promoted.get(node)?.visual.layer ?? null,
|
|
242
256
|
onIdle: (node) => this._idle(node),
|
|
243
257
|
});
|
|
258
|
+
// the parts of elements' drawing they offer (`Node.sprites()`), each on
|
|
259
|
+
// a layer of its own under the same rules (src/cocoa/sprites.js)
|
|
260
|
+
this.sprites = new SpriteLayers(this);
|
|
244
261
|
this._claiming = false;
|
|
245
262
|
}
|
|
246
263
|
|
|
@@ -388,8 +405,19 @@ export class CocoaPromotion {
|
|
|
388
405
|
*/
|
|
389
406
|
frame(root, layoutRan) {
|
|
390
407
|
if (layoutRan) this.layoutGen++;
|
|
391
|
-
|
|
408
|
+
// what elements offer, asked before anything is decided: an answer is
|
|
409
|
+
// about the element's own drawing, not about the layers
|
|
410
|
+
const offers = this.sprites.ask(root);
|
|
411
|
+
if (
|
|
412
|
+
this.promoted.size === 0 &&
|
|
413
|
+
this.candidates.size === 0 &&
|
|
414
|
+
offers.size === 0 &&
|
|
415
|
+
this.sprites.hosts.size === 0
|
|
416
|
+
) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
392
419
|
const native = this.native;
|
|
420
|
+
let lifted = null;
|
|
393
421
|
native.txBegin({ disableActions: true });
|
|
394
422
|
try {
|
|
395
423
|
// Later-painted first: a node painted after this one that is coming
|
|
@@ -414,13 +442,26 @@ export class CocoaPromotion {
|
|
|
414
442
|
}
|
|
415
443
|
}
|
|
416
444
|
}
|
|
445
|
+
// Then the parts, against the layers this frame keeps: a node
|
|
446
|
+
// promoted over a part is a layer above it, not ink on it.
|
|
447
|
+
lifted = this.sprites.decide(root, offers);
|
|
448
|
+
// One order for every layer above the bitmap, nodes and parts alike.
|
|
417
449
|
const order = this._inPaintOrder([...this.promoted.keys()]);
|
|
450
|
+
for (const state of this.sprites.entries()) {
|
|
451
|
+
order.push({ state, key: state.order });
|
|
452
|
+
}
|
|
453
|
+
order.sort(byPaintKey);
|
|
418
454
|
for (let i = 0; i < order.length; i++) {
|
|
419
|
-
|
|
455
|
+
const { node, state } = order[i];
|
|
456
|
+
if (node) this._sync(node, i, root, layoutRan);
|
|
457
|
+
else this.sprites.sync(state, i);
|
|
420
458
|
}
|
|
421
459
|
} finally {
|
|
422
460
|
native.txCommit();
|
|
423
461
|
}
|
|
462
|
+
// An element told which of its parts are on layers now may claim,
|
|
463
|
+
// restyle or ask for frames; the frame has not taken its damage yet.
|
|
464
|
+
if (lifted?.size) this.sprites.notify(lifted);
|
|
424
465
|
}
|
|
425
466
|
|
|
426
467
|
_mayStay(node, key) {
|
|
@@ -438,7 +479,9 @@ export class CocoaPromotion {
|
|
|
438
479
|
dirty: { all: true, rects: [] },
|
|
439
480
|
});
|
|
440
481
|
node._promoted = true;
|
|
441
|
-
// the bitmap under it repaints without it, from this frame on
|
|
482
|
+
// the bitmap under it repaints without it, from this frame on — and so
|
|
483
|
+
// does the raster of a promoted node it is in, which the claim that
|
|
484
|
+
// handed us its animation reached (`noteInvalidate`)
|
|
442
485
|
this._claim(root, node.paintBounds());
|
|
443
486
|
}
|
|
444
487
|
|
|
@@ -457,7 +500,32 @@ export class CocoaPromotion {
|
|
|
457
500
|
this.animations.drop(node, reclaim && !node.destroyed);
|
|
458
501
|
this._dropContent(p);
|
|
459
502
|
p.visual.destroy();
|
|
460
|
-
if (!node.destroyed && node.abs)
|
|
503
|
+
if (!node.destroyed && node.abs) {
|
|
504
|
+
this._repaintHolder(node);
|
|
505
|
+
this._claim(root, node.paintBounds());
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* A node coming off its layer inside a promoted node: the bitmap holds a
|
|
511
|
+
* hole there, and the node's pixels belong in that node's raster — so the
|
|
512
|
+
* raster repaints where the node reaches, taking it back, in this frame.
|
|
513
|
+
* Nothing else would ask: `_claim` keeps our claims off our own books, a
|
|
514
|
+
* raster's layout key is compared only in a frame that ran layout, and
|
|
515
|
+
* what gives a node back claims its own layer (the grace ran out) or
|
|
516
|
+
* nothing (an ancestor began to fade, a sibling came over it). Going onto
|
|
517
|
+
* a layer needs no such call — the claim that handed us the animation
|
|
518
|
+
* reached the raster. The nearest promoted node alone, since a raster
|
|
519
|
+
* leaves out the whole subtree of a promoted child.
|
|
520
|
+
*/
|
|
521
|
+
_repaintHolder(node) {
|
|
522
|
+
for (let n = node.parent; n && !n.isWindow; n = n.parent) {
|
|
523
|
+
const holder = this.promoted.get(n);
|
|
524
|
+
if (holder) {
|
|
525
|
+
this._dirtyRect(holder, node.paintBounds());
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
461
529
|
}
|
|
462
530
|
|
|
463
531
|
_dropContent(p) {
|
|
@@ -499,29 +567,36 @@ export class CocoaPromotion {
|
|
|
499
567
|
|
|
500
568
|
_inPaintOrder(nodes) {
|
|
501
569
|
const keyed = nodes.map((node) => ({ node, key: this._paintKey(node) }));
|
|
502
|
-
keyed.sort(
|
|
503
|
-
if (!a.key || !b.key) return (a.key ? 1 : 0) - (b.key ? 1 : 0);
|
|
504
|
-
const n = Math.min(a.key.length, b.key.length);
|
|
505
|
-
for (let i = 0; i < n; i++) {
|
|
506
|
-
if (a.key[i] !== b.key[i]) return a.key[i] - b.key[i];
|
|
507
|
-
}
|
|
508
|
-
return a.key.length - b.key.length;
|
|
509
|
-
});
|
|
570
|
+
keyed.sort(byPaintKey);
|
|
510
571
|
return keyed;
|
|
511
572
|
}
|
|
512
573
|
|
|
513
574
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
|
|
575
|
+
* Where an element offering parts stands in the paint order, or null when
|
|
576
|
+
* none of them may go on a layer: hidden, inside a `<glarea>`, or itself
|
|
577
|
+
* faded — its group on the bitmap would leave a part's layer out of it. A
|
|
578
|
+
* faded ancestor is `_clear`'s to find, as it is for a node.
|
|
579
|
+
*/
|
|
580
|
+
_spritePlace(host) {
|
|
581
|
+
if (host.destroyed || host.hidden) return null;
|
|
582
|
+
if (insideGlArea(host) || fadesAsGroup(host)) return null;
|
|
583
|
+
return this._paintKey(host);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Is nothing painted over this node, is all of it visible, and is it in
|
|
588
|
+
* no faded group? Walked up the chain: at every level, what the parent
|
|
589
|
+
* paints after the child on the way to this node — later siblings, then
|
|
590
|
+
* the parent's own border and ring, then its bars — must keep out of the
|
|
591
|
+
* node's reach, the parent's clip, if it has one, must hold the whole of
|
|
592
|
+
* it, and the parent must not fade: its group is drawn on the bitmap or on
|
|
593
|
+
* its own layer, and either way a layer of the node's would be outside it.
|
|
519
594
|
*/
|
|
520
|
-
_clear(node) {
|
|
595
|
+
_clear(node, bounds = node._subtreeBounds()) {
|
|
521
596
|
// the exact reach, not `paintBounds()`: that one carries the damage
|
|
522
597
|
// model's pixel of slop, and a section laid out flush under a card
|
|
523
|
-
// would read as reaching into it
|
|
524
|
-
|
|
598
|
+
// would read as reaching into it. An element's part is asked about at
|
|
599
|
+
// everywhere it can draw (src/cocoa/sprites.js).
|
|
525
600
|
for (let n = node; !n.isWindow; n = n.parent) {
|
|
526
601
|
const parent = n.parent;
|
|
527
602
|
if (!parent) return false;
|
|
@@ -530,6 +605,7 @@ export class CocoaPromotion {
|
|
|
530
605
|
if (this._reaches(order[j], bounds)) return false;
|
|
531
606
|
}
|
|
532
607
|
if (!parent.isWindow) {
|
|
608
|
+
if (fadesAsGroup(parent)) return false;
|
|
533
609
|
const border = borderReach(parent);
|
|
534
610
|
if (
|
|
535
611
|
border > 0 &&
|
|
@@ -701,6 +777,7 @@ export class CocoaPromotion {
|
|
|
701
777
|
|
|
702
778
|
/** Everything off the root layer and freed: the window is going. */
|
|
703
779
|
destroy() {
|
|
780
|
+
this.sprites.destroy();
|
|
704
781
|
for (const timer of this.idle.values()) clearTimeout(timer);
|
|
705
782
|
this.idle.clear();
|
|
706
783
|
this.releasing.clear();
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
// Sprites on the surface presenter (issue #819): the parts of an element's
|
|
2
|
+
// drawing it offers (`Node.sprites()`, src/nodes/sprites.js), each on a
|
|
3
|
+
// CALayer of its own above the window's bitmap, with the animations it
|
|
4
|
+
// carries run by the render server — a CSS animation inside `<Html>`, say,
|
|
5
|
+
// at no JavaScript frame at all.
|
|
6
|
+
//
|
|
7
|
+
// It is layer promotion with an element's part in place of a node, and it
|
|
8
|
+
// is promotion's to run (src/cocoa/promotion.js): the same z-order test,
|
|
9
|
+
// asked of the element at everywhere the part can draw (`_clear`), the same
|
|
10
|
+
// claim of the bitmap under it on the way onto a layer and off it, one order
|
|
11
|
+
// among every layer above the bitmap, and the same rule that declining is
|
|
12
|
+
// always safe — a part the frame refuses is one the element draws itself,
|
|
13
|
+
// as it would anywhere else. What is the element's and nobody else's is its
|
|
14
|
+
// inside: it offers only a part nothing it draws after the part overlaps,
|
|
15
|
+
// and it leaves a hole where a lifted one is (`spritesLifted`).
|
|
16
|
+
//
|
|
17
|
+
// A part's content is a raster the presenter paints by calling the part's
|
|
18
|
+
// own `paint`, at opacity 1 and untransformed, and paints again when its
|
|
19
|
+
// `version` changes; its opacity and its transform are the layer's, and so
|
|
20
|
+
// are its animations, keyframes the bridge hands to Core Animation. A
|
|
21
|
+
// transform is CSS's matrix, which the bridge takes from
|
|
22
|
+
// `@windowkit/appkit` 0.19 (`transformForms()`) along with the negative
|
|
23
|
+
// delay a part joined half way through needs, so that is the bridge sprites
|
|
24
|
+
// ask for: on an older one nothing is asked and every part is drawn by its
|
|
25
|
+
// element.
|
|
26
|
+
|
|
27
|
+
import { RASTER_PAD, RasterState } from './presenter.js';
|
|
28
|
+
import { DEV } from '../nodes/util.js';
|
|
29
|
+
|
|
30
|
+
// Past this many device pixels on a side a part is drawn by its element: a
|
|
31
|
+
// layer's contents are a bitmap, and a page-sized one is a page-sized
|
|
32
|
+
// upload for every new version.
|
|
33
|
+
const MAX_SIDE = 8192;
|
|
34
|
+
|
|
35
|
+
// Sorted after the element's children, which its own drawing comes after
|
|
36
|
+
// (`Node.paint` paints the children first), and before its next sibling.
|
|
37
|
+
const AFTER_CHILDREN = Number.MAX_SAFE_INTEGER;
|
|
38
|
+
|
|
39
|
+
const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
40
|
+
|
|
41
|
+
// what a part's raster holds before its first paint
|
|
42
|
+
const UNPAINTED = Symbol('unpainted');
|
|
43
|
+
|
|
44
|
+
// the ids the bridge reports an animation's end under: unique per process,
|
|
45
|
+
// looked up per app (`_animationEnds`), like LayerAnimations'
|
|
46
|
+
let spriteSeq = 0;
|
|
47
|
+
|
|
48
|
+
const finite = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
49
|
+
|
|
50
|
+
const isRect = (r) =>
|
|
51
|
+
r != null &&
|
|
52
|
+
finite(r.x) &&
|
|
53
|
+
finite(r.y) &&
|
|
54
|
+
finite(r.width) &&
|
|
55
|
+
finite(r.height) &&
|
|
56
|
+
r.width > 0 &&
|
|
57
|
+
r.height > 0;
|
|
58
|
+
|
|
59
|
+
const isMatrix = (m) => Array.isArray(m) && m.length === 6 && m.every(finite);
|
|
60
|
+
|
|
61
|
+
const isCurve = (t) =>
|
|
62
|
+
typeof t === 'string' ||
|
|
63
|
+
(Array.isArray(t) &&
|
|
64
|
+
t.length === 4 &&
|
|
65
|
+
t.every(finite) &&
|
|
66
|
+
t[0] >= 0 &&
|
|
67
|
+
t[0] <= 1 &&
|
|
68
|
+
t[2] >= 0 &&
|
|
69
|
+
t[2] <= 1);
|
|
70
|
+
|
|
71
|
+
/** Is `a` an animation the bridge will take as it is — so that a bad one
|
|
72
|
+
* is refused here rather than thrown from inside a frame? */
|
|
73
|
+
function isAnimation(a) {
|
|
74
|
+
if (a == null || typeof a.id !== 'string') return false;
|
|
75
|
+
if (a.property !== 'opacity' && a.property !== 'transform') return false;
|
|
76
|
+
if (!finite(a.duration) || !(a.duration > 0)) return false;
|
|
77
|
+
const values = a.values;
|
|
78
|
+
if (!Array.isArray(values) || values.length < 2) return false;
|
|
79
|
+
const value = a.property === 'opacity' ? finite : isMatrix;
|
|
80
|
+
if (!values.every(value)) return false;
|
|
81
|
+
if (a.keyTimes !== undefined) {
|
|
82
|
+
const t = a.keyTimes;
|
|
83
|
+
if (!Array.isArray(t) || t.length !== values.length || t[0] !== 0) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < t.length; i++) {
|
|
87
|
+
if (!finite(t[i]) || t[i] > 1 || (i > 0 && t[i] < t[i - 1])) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (a.timings !== undefined) {
|
|
93
|
+
const t = a.timings;
|
|
94
|
+
if (!Array.isArray(t) || t.length !== values.length - 1) return false;
|
|
95
|
+
if (!t.every(isCurve)) return false;
|
|
96
|
+
}
|
|
97
|
+
if (a.delay !== undefined && !finite(a.delay)) return false;
|
|
98
|
+
if (
|
|
99
|
+
a.repeat !== undefined &&
|
|
100
|
+
!(a.repeat === Infinity || (finite(a.repeat) && a.repeat > 0))
|
|
101
|
+
) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Is `sprite` a part as `Node.sprites()` documents one? */
|
|
108
|
+
function isSprite(sprite) {
|
|
109
|
+
if (sprite == null || typeof sprite.key !== 'string') return false;
|
|
110
|
+
if (!isRect(sprite.rect) || typeof sprite.paint !== 'function') return false;
|
|
111
|
+
if (sprite.reach !== undefined && !isRect(sprite.reach)) return false;
|
|
112
|
+
if (sprite.opacity !== undefined && !finite(sprite.opacity)) return false;
|
|
113
|
+
if (sprite.transform !== undefined && !isMatrix(sprite.transform)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
sprite.origin !== undefined &&
|
|
118
|
+
!(
|
|
119
|
+
sprite.origin != null &&
|
|
120
|
+
finite(sprite.origin.x) &&
|
|
121
|
+
finite(sprite.origin.y)
|
|
122
|
+
)
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const animations = sprite.animations;
|
|
127
|
+
if (animations !== undefined) {
|
|
128
|
+
if (!Array.isArray(animations) || !animations.every(isAnimation)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const originOf = (sprite) =>
|
|
136
|
+
sprite.origin ?? {
|
|
137
|
+
x: sprite.rect.x + sprite.rect.width / 2,
|
|
138
|
+
y: sprite.rect.y + sprite.rect.height / 2,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** `rect` through `m` about `origin`: the bounds of its four corners. */
|
|
142
|
+
function mapRect(rect, m, origin) {
|
|
143
|
+
let x0 = Infinity;
|
|
144
|
+
let y0 = Infinity;
|
|
145
|
+
let x1 = -Infinity;
|
|
146
|
+
let y1 = -Infinity;
|
|
147
|
+
for (const [px, py] of [
|
|
148
|
+
[rect.x, rect.y],
|
|
149
|
+
[rect.x + rect.width, rect.y],
|
|
150
|
+
[rect.x, rect.y + rect.height],
|
|
151
|
+
[rect.x + rect.width, rect.y + rect.height],
|
|
152
|
+
]) {
|
|
153
|
+
const dx = px - origin.x;
|
|
154
|
+
const dy = py - origin.y;
|
|
155
|
+
const x = m[0] * dx + m[2] * dy + m[4] + origin.x;
|
|
156
|
+
const y = m[1] * dx + m[3] * dy + m[5] + origin.y;
|
|
157
|
+
if (x < x0) x0 = x;
|
|
158
|
+
if (y < y0) y0 = y;
|
|
159
|
+
if (x > x1) x1 = x;
|
|
160
|
+
if (y > y1) y1 = y;
|
|
161
|
+
}
|
|
162
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function unionRect(a, b) {
|
|
166
|
+
if (!a) return b;
|
|
167
|
+
const x = Math.min(a.x, b.x);
|
|
168
|
+
const y = Math.min(a.y, b.y);
|
|
169
|
+
return {
|
|
170
|
+
x,
|
|
171
|
+
y,
|
|
172
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
173
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Everywhere `sprite` can draw while its animations run: its reach through
|
|
179
|
+
* its own transform and every transform it is animated through, out to
|
|
180
|
+
* whole pixels and the antialiasing a raster is padded for. Between two
|
|
181
|
+
* keyframes Core Animation interpolates the matrices by their parts, which
|
|
182
|
+
* stays within a pixel of the two for the densely sampled keyframes a CSS
|
|
183
|
+
* transform arrives as; an element whose keyframes are far apart, or whose
|
|
184
|
+
* curve overshoots, offers a `reach` that covers what is between them.
|
|
185
|
+
*/
|
|
186
|
+
function extentOf(sprite) {
|
|
187
|
+
const reach = sprite.reach ?? sprite.rect;
|
|
188
|
+
const origin = originOf(sprite);
|
|
189
|
+
let box = mapRect(reach, sprite.transform ?? IDENTITY, origin);
|
|
190
|
+
for (const a of sprite.animations ?? []) {
|
|
191
|
+
if (a.property !== 'transform') continue;
|
|
192
|
+
for (const m of a.values) box = unionRect(box, mapRect(reach, m, origin));
|
|
193
|
+
}
|
|
194
|
+
const x = Math.floor(box.x) - RASTER_PAD;
|
|
195
|
+
const y = Math.floor(box.y) - RASTER_PAD;
|
|
196
|
+
return {
|
|
197
|
+
x,
|
|
198
|
+
y,
|
|
199
|
+
width: Math.ceil(box.x + box.width) + RASTER_PAD - x,
|
|
200
|
+
height: Math.ceil(box.y + box.height) + RASTER_PAD - y,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The raster a part's content is painted into: its reach, out to whole
|
|
205
|
+
* pixels and padded for antialiasing, untransformed. */
|
|
206
|
+
function rasterRectOf(sprite) {
|
|
207
|
+
const reach = sprite.reach ?? sprite.rect;
|
|
208
|
+
const x = Math.floor(reach.x) - RASTER_PAD;
|
|
209
|
+
const y = Math.floor(reach.y) - RASTER_PAD;
|
|
210
|
+
return {
|
|
211
|
+
x,
|
|
212
|
+
y,
|
|
213
|
+
width: Math.ceil(reach.x + reach.width) + RASTER_PAD - x,
|
|
214
|
+
height: Math.ceil(reach.y + reach.height) + RASTER_PAD - y,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const sameMatrix = (a, b) =>
|
|
219
|
+
a === b || (a != null && b != null && a.every((v, i) => v === b[i]));
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The lifted parts of every element in one window: a layer each, its raster,
|
|
223
|
+
* and what the render server runs on it. `CocoaPromotion` asks, decides and
|
|
224
|
+
* syncs through it inside its own frame.
|
|
225
|
+
*/
|
|
226
|
+
export class SpriteLayers {
|
|
227
|
+
constructor(promotion) {
|
|
228
|
+
this.promotion = promotion;
|
|
229
|
+
this.native = promotion.native;
|
|
230
|
+
this.scale = promotion.scale;
|
|
231
|
+
this.app = promotion.app;
|
|
232
|
+
// a matrix and a negative delay are what a CSS animation needs from the
|
|
233
|
+
// bridge, and @windowkit/appkit 0.19 is where both are
|
|
234
|
+
this.supported = Boolean(
|
|
235
|
+
this.native.transformForms?.()?.includes?.('matrix'),
|
|
236
|
+
);
|
|
237
|
+
this.hosts = new Map(); // element -> Map(key -> state)
|
|
238
|
+
this._warned = new WeakSet();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_ends() {
|
|
242
|
+
return (this.app._animationEnds ??= new Map());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_warn(host, message) {
|
|
246
|
+
if (!DEV || this._warned.has(host)) return;
|
|
247
|
+
this._warned.add(host);
|
|
248
|
+
console.warn(`react-x11: <${host.kind}> ${message}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Every element's answer this frame, before anything is decided: the
|
|
253
|
+
* elements that offer a part, and what they offer. An element that throws
|
|
254
|
+
* offers nothing and draws everything, which is what it would do anywhere
|
|
255
|
+
* else.
|
|
256
|
+
*/
|
|
257
|
+
ask(root) {
|
|
258
|
+
const offers = new Map();
|
|
259
|
+
if (!this.supported) return offers;
|
|
260
|
+
for (const host of root._spriteNodes ?? []) {
|
|
261
|
+
if (host.destroyed) continue;
|
|
262
|
+
let list = null;
|
|
263
|
+
try {
|
|
264
|
+
list = host.sprites();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
this._warn(
|
|
267
|
+
host,
|
|
268
|
+
`sprites() threw, and its parts are drawn by it: ${err?.message ?? err}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (Array.isArray(list) && list.length !== 0) offers.set(host, list);
|
|
272
|
+
}
|
|
273
|
+
return offers;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Which offered parts go on layers this frame, against the scene as laid
|
|
278
|
+
* out and the layers promotion has decided on: a part is lifted, kept, or
|
|
279
|
+
* given back. Answers the elements whose set of lifted parts changed.
|
|
280
|
+
*/
|
|
281
|
+
decide(root, offers) {
|
|
282
|
+
const changed = new Set();
|
|
283
|
+
for (const [host, lifted] of this.hosts) {
|
|
284
|
+
if (offers.has(host)) continue;
|
|
285
|
+
for (const state of lifted.values()) this._drop(state, root);
|
|
286
|
+
this.hosts.delete(host);
|
|
287
|
+
if (!host.destroyed) changed.add(host);
|
|
288
|
+
}
|
|
289
|
+
for (const [host, list] of offers) {
|
|
290
|
+
const was = this.hosts.get(host);
|
|
291
|
+
const now = new Map();
|
|
292
|
+
// where the element stands in the paint order, or null where none of
|
|
293
|
+
// it may go on a layer: hidden, faded, inside a <glarea>
|
|
294
|
+
const place = this.promotion._spritePlace(host);
|
|
295
|
+
for (let index = 0; place && index < list.length; index++) {
|
|
296
|
+
const sprite = list[index];
|
|
297
|
+
if (!isSprite(sprite)) {
|
|
298
|
+
this._warn(
|
|
299
|
+
host,
|
|
300
|
+
`offered a sprite that is not one (${JSON.stringify(sprite?.key)}); see Node.sprites()`,
|
|
301
|
+
);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (now.has(sprite.key)) continue;
|
|
305
|
+
const raster = rasterRectOf(sprite);
|
|
306
|
+
if (raster.width > MAX_SIDE || raster.height > MAX_SIDE) continue;
|
|
307
|
+
const extent = extentOf(sprite);
|
|
308
|
+
if (!this.promotion._clear(host, extent)) continue;
|
|
309
|
+
let state = was?.get(sprite.key);
|
|
310
|
+
const lifting = !state;
|
|
311
|
+
if (lifting) state = this._lift(host, sprite.key);
|
|
312
|
+
state.sprite = sprite;
|
|
313
|
+
state.order = [...place, AFTER_CHILDREN, index];
|
|
314
|
+
// the bitmap under where it can be repaints without it, from this
|
|
315
|
+
// frame on; where it went since, it is the element's own claims
|
|
316
|
+
// that move the hole
|
|
317
|
+
if (lifting) this.promotion._claim(root, extent);
|
|
318
|
+
state.extent = extent;
|
|
319
|
+
now.set(sprite.key, state);
|
|
320
|
+
}
|
|
321
|
+
if (was) {
|
|
322
|
+
for (const [key, state] of was) {
|
|
323
|
+
if (!now.has(key)) this._drop(state, root);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (now.size !== 0) this.hosts.set(host, now);
|
|
327
|
+
else this.hosts.delete(host);
|
|
328
|
+
const before = was?.size ?? 0;
|
|
329
|
+
if (before !== now.size || [...now.keys()].some((k) => !was.has(k))) {
|
|
330
|
+
changed.add(host);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return changed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Every lifted part, with its place in the paint order. */
|
|
337
|
+
entries() {
|
|
338
|
+
const out = [];
|
|
339
|
+
for (const lifted of this.hosts.values()) {
|
|
340
|
+
for (const state of lifted.values()) out.push(state);
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Tell each element that changed which of its parts are lifted now —
|
|
346
|
+
* after the decisions, before the frame paints. */
|
|
347
|
+
notify(changed) {
|
|
348
|
+
for (const host of changed) {
|
|
349
|
+
if (host.destroyed) continue;
|
|
350
|
+
const keys = new Set(this.hosts.get(host)?.keys() ?? []);
|
|
351
|
+
try {
|
|
352
|
+
host.spritesLifted(keys);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this._warn(host, `spritesLifted() threw: ${err?.message ?? err}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
_lift(host, key) {
|
|
360
|
+
const layer = this.native.createLayer();
|
|
361
|
+
this.native.addSublayer(this.promotion.rootVisual.layer, layer);
|
|
362
|
+
return {
|
|
363
|
+
host,
|
|
364
|
+
key,
|
|
365
|
+
layer,
|
|
366
|
+
raster: new RasterState(),
|
|
367
|
+
painted: UNPAINTED,
|
|
368
|
+
rect: null, // the raster's rect, device pixels, window coordinates
|
|
369
|
+
phase: null, // where in a pixel its reach starts
|
|
370
|
+
props: {},
|
|
371
|
+
runs: new Map(), // element's animation id -> { caId, caKey, held }
|
|
372
|
+
// ids the render server is done with, while the element still lists
|
|
373
|
+
// them: an animation is attached once per id, and an end the element
|
|
374
|
+
// has not caught up with must not start it again
|
|
375
|
+
done: new Set(),
|
|
376
|
+
sprite: null,
|
|
377
|
+
extent: null,
|
|
378
|
+
order: null,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Off its layer: the layer and its raster go, its animations are
|
|
383
|
+
* forgotten, and the bitmap under where it could be repaints with the
|
|
384
|
+
* element drawing it again. */
|
|
385
|
+
_drop(state, root) {
|
|
386
|
+
this.native.removeFromSuperlayer(state.layer);
|
|
387
|
+
state.raster.release(this.native);
|
|
388
|
+
for (const run of state.runs.values()) this._ends().delete(run.caId);
|
|
389
|
+
state.runs.clear();
|
|
390
|
+
if (!state.host.destroyed && state.extent) {
|
|
391
|
+
this.promotion._claim(root, state.extent);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The layer as the part says, at `z` among the layers above the bitmap. */
|
|
396
|
+
sync(state, z) {
|
|
397
|
+
const sprite = state.sprite;
|
|
398
|
+
const s = this.scale;
|
|
399
|
+
const rect = rasterRectOf(sprite);
|
|
400
|
+
const reach = sprite.reach ?? sprite.rect;
|
|
401
|
+
const phase = `${reach.x - Math.floor(reach.x)},${reach.y - Math.floor(reach.y)}`;
|
|
402
|
+
// painted again for a new version, and for a raster of another size or
|
|
403
|
+
// with the part at another fraction of a pixel; a move by whole pixels
|
|
404
|
+
// is the layer's alone
|
|
405
|
+
if (
|
|
406
|
+
state.painted === UNPAINTED ||
|
|
407
|
+
sprite.version !== state.painted ||
|
|
408
|
+
state.rect?.width !== rect.width ||
|
|
409
|
+
state.rect?.height !== rect.height ||
|
|
410
|
+
state.phase !== phase
|
|
411
|
+
) {
|
|
412
|
+
this._paint(state, sprite, rect);
|
|
413
|
+
state.phase = phase;
|
|
414
|
+
}
|
|
415
|
+
state.rect = rect;
|
|
416
|
+
const origin = originOf(sprite);
|
|
417
|
+
const m = sprite.transform ?? IDENTITY;
|
|
418
|
+
const next = {
|
|
419
|
+
bounds: [0, 0, rect.width / s, rect.height / s],
|
|
420
|
+
anchorPoint: [
|
|
421
|
+
(origin.x - rect.x) / rect.width,
|
|
422
|
+
(origin.y - rect.y) / rect.height,
|
|
423
|
+
],
|
|
424
|
+
position: [origin.x / s, origin.y / s],
|
|
425
|
+
zPosition: z,
|
|
426
|
+
opacity: Math.min(1, Math.max(0, sprite.opacity ?? 1)),
|
|
427
|
+
matrix: m,
|
|
428
|
+
};
|
|
429
|
+
const prev = state.props;
|
|
430
|
+
const out = {};
|
|
431
|
+
let any = false;
|
|
432
|
+
for (const key of ['bounds', 'anchorPoint', 'position']) {
|
|
433
|
+
if (!sameMatrix(prev[key], next[key])) {
|
|
434
|
+
out[key] = next[key];
|
|
435
|
+
any = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const key of ['zPosition', 'opacity']) {
|
|
439
|
+
if (prev[key] !== next[key]) {
|
|
440
|
+
out[key] = next[key];
|
|
441
|
+
any = true;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!sameMatrix(prev.matrix, m)) {
|
|
445
|
+
out.transform = { matrix: [m[0], m[1], m[2], m[3], m[4] / s, m[5] / s] };
|
|
446
|
+
any = true;
|
|
447
|
+
}
|
|
448
|
+
if (any) this.native.setLayerProps(state.layer, out);
|
|
449
|
+
state.props = next;
|
|
450
|
+
// after the model went out, in the same transaction
|
|
451
|
+
this._syncAnimations(state, sprite);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
_paint(state, sprite, rect) {
|
|
455
|
+
const ctx = state.raster.ensure(
|
|
456
|
+
this.promotion,
|
|
457
|
+
rect.width,
|
|
458
|
+
rect.height,
|
|
459
|
+
this.scale,
|
|
460
|
+
);
|
|
461
|
+
const root = state.host.root;
|
|
462
|
+
ctx.save();
|
|
463
|
+
try {
|
|
464
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
465
|
+
ctx.translate(-rect.x, -rect.y);
|
|
466
|
+
// a part paints whole: there is no pass to cull it by
|
|
467
|
+
if (root) root._paintDamage = null;
|
|
468
|
+
sprite.paint(ctx);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
this._warn(
|
|
471
|
+
state.host,
|
|
472
|
+
`a sprite's paint() threw: ${err?.message ?? err}`,
|
|
473
|
+
);
|
|
474
|
+
} finally {
|
|
475
|
+
ctx.restore();
|
|
476
|
+
}
|
|
477
|
+
this.native.surfaceToLayer(state.raster.surface, state.layer);
|
|
478
|
+
state.painted = sprite.version;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** What runs on the layer, by the element's ids: one gone from the part
|
|
482
|
+
* comes off, one new to it goes on, and one already on is left alone —
|
|
483
|
+
* its `delay` is counted from the frame that asks, so it moves every
|
|
484
|
+
* frame and means nothing past the first. */
|
|
485
|
+
_syncAnimations(state, sprite) {
|
|
486
|
+
const wanted = sprite.animations ?? [];
|
|
487
|
+
const listed = (id) => wanted.some((a) => a.id === id);
|
|
488
|
+
for (const [id, run] of state.runs) {
|
|
489
|
+
if (listed(id)) continue;
|
|
490
|
+
this.native.removeAnimation(state.layer, run.caKey);
|
|
491
|
+
this._ends().delete(run.caId);
|
|
492
|
+
state.runs.delete(id);
|
|
493
|
+
}
|
|
494
|
+
for (const id of state.done) if (!listed(id)) state.done.delete(id);
|
|
495
|
+
const s = this.scale;
|
|
496
|
+
for (const a of wanted) {
|
|
497
|
+
if (state.runs.has(a.id) || state.done.has(a.id)) continue;
|
|
498
|
+
const caId = `rxs${++spriteSeq}`;
|
|
499
|
+
const caKey = `sprite:${caId}`;
|
|
500
|
+
const opts = {
|
|
501
|
+
values:
|
|
502
|
+
a.property === 'transform'
|
|
503
|
+
? a.values.map((m) => ({
|
|
504
|
+
matrix: [m[0], m[1], m[2], m[3], m[4] / s, m[5] / s],
|
|
505
|
+
}))
|
|
506
|
+
: a.values,
|
|
507
|
+
duration: a.duration / 1000,
|
|
508
|
+
id: caId,
|
|
509
|
+
};
|
|
510
|
+
if (a.keyTimes) opts.keyTimes = a.keyTimes;
|
|
511
|
+
if (a.timings) opts.timings = a.timings;
|
|
512
|
+
if (a.repeat !== undefined) opts.repeat = a.repeat;
|
|
513
|
+
if (a.autoreverse) opts.autoreverse = true;
|
|
514
|
+
if (a.hold) opts.hold = true;
|
|
515
|
+
// from the frame that asked: ahead, or already that far in
|
|
516
|
+
if (a.delay) opts.delay = a.delay / 1000;
|
|
517
|
+
try {
|
|
518
|
+
this.native.addAnimation(state.layer, a.property, opts, caKey);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
this._warn(
|
|
521
|
+
state.host,
|
|
522
|
+
`the bridge refused sprite animation ${JSON.stringify(a.id)}: ${err?.message ?? err}`,
|
|
523
|
+
);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
state.runs.set(a.id, { caId, caKey, held: Boolean(a.hold) });
|
|
527
|
+
this._ends().set(caId, (ev) => this._ended(state, a.id, caId, ev));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The bridge's `animation-end` for one of ours. A held one stays on the
|
|
532
|
+
* layer, showing its last value, until the element stops listing it. */
|
|
533
|
+
_ended(state, id, caId, ev) {
|
|
534
|
+
this._ends().delete(caId);
|
|
535
|
+
const run = state.runs.get(id);
|
|
536
|
+
if (run?.caId === caId) {
|
|
537
|
+
state.done.add(id);
|
|
538
|
+
if (!run.held) state.runs.delete(id);
|
|
539
|
+
}
|
|
540
|
+
if (state.host.destroyed) return;
|
|
541
|
+
try {
|
|
542
|
+
state.host.spriteAnimationEnded(state.key, id, ev?.finished !== false);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
this._warn(
|
|
545
|
+
state.host,
|
|
546
|
+
`spriteAnimationEnded() threw: ${err?.message ?? err}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Everything off the root and freed: the window is going. */
|
|
552
|
+
destroy() {
|
|
553
|
+
for (const lifted of this.hosts.values()) {
|
|
554
|
+
for (const state of lifted.values()) {
|
|
555
|
+
this.native.removeFromSuperlayer(state.layer);
|
|
556
|
+
state.raster.release(this.native);
|
|
557
|
+
for (const run of state.runs.values()) this._ends().delete(run.caId);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
this.hosts.clear();
|
|
561
|
+
}
|
|
562
|
+
}
|
package/src/cocoa/window.js
CHANGED
|
@@ -260,6 +260,10 @@ export class CocoaWindow {
|
|
|
260
260
|
this._promotion.noteInvalidate(damage, layoutChanged);
|
|
261
261
|
this.prepareFrame = (root, layoutRan) =>
|
|
262
262
|
this._promotion.frame(root, layoutRan);
|
|
263
|
+
// An element's parts on layers of their own (src/cocoa/sprites.js)
|
|
264
|
+
// are asked for in that frame, so an element whose parts changed with
|
|
265
|
+
// nothing else to paint asks for one.
|
|
266
|
+
this.spritesChanged = () => this._reactX11Node?._scheduleFrame();
|
|
263
267
|
}
|
|
264
268
|
app._registerWindow(this);
|
|
265
269
|
}
|
package/src/node.d.ts
CHANGED
|
@@ -230,6 +230,75 @@ export interface PaintCachePlan {
|
|
|
230
230
|
tint?: string;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* CSS's `matrix(a, b, c, d, e, f)`: a point (x, y) goes to
|
|
235
|
+
* (a·x + c·y + e, b·x + d·y + f), with `e` and `f` in device pixels.
|
|
236
|
+
*/
|
|
237
|
+
export type SpriteMatrix = [number, number, number, number, number, number];
|
|
238
|
+
|
|
239
|
+
/** A cubic-bezier's control points `[x1, y1, x2, y2]`, `x1` and `x2` in
|
|
240
|
+
* 0..1, or one of Core Animation's curve names. */
|
|
241
|
+
export type SpriteTiming = [number, number, number, number] | string;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* An animation the render server runs on a sprite: keyframes for one
|
|
245
|
+
* property, played as CSS plays an animation's iterations.
|
|
246
|
+
*/
|
|
247
|
+
export interface SpriteAnimation {
|
|
248
|
+
/** The element's own name for this run, which `spriteAnimationEnded`
|
|
249
|
+
* reports. An animation is attached once per id: to change one, give it
|
|
250
|
+
* a new id. */
|
|
251
|
+
id: string;
|
|
252
|
+
property: 'opacity' | 'transform';
|
|
253
|
+
/** At least two: opacities, or matrices for a transform — sampled densely
|
|
254
|
+
* where the element interpolates in a way the render server would not. */
|
|
255
|
+
values: number[] | SpriteMatrix[];
|
|
256
|
+
/** One per value, 0 to 1, never decreasing; evenly spaced when left out. */
|
|
257
|
+
keyTimes?: number[];
|
|
258
|
+
/** One curve per segment, so one fewer than `values`; linear when left
|
|
259
|
+
* out. */
|
|
260
|
+
timings?: SpriteTiming[];
|
|
261
|
+
/** Milliseconds, one iteration. */
|
|
262
|
+
duration: number;
|
|
263
|
+
/** Milliseconds from the frame that asks: ahead, or — negative — already
|
|
264
|
+
* that far in. Read when the animation is attached and not after. */
|
|
265
|
+
delay?: number;
|
|
266
|
+
/** Iterations, `Infinity` for a loop; one when left out. */
|
|
267
|
+
repeat?: number;
|
|
268
|
+
/** Each iteration turns round at its end, as `alternate` does. */
|
|
269
|
+
autoreverse?: boolean;
|
|
270
|
+
/** The last value stays on screen after the end. */
|
|
271
|
+
hold?: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A part of an element's drawing a presenter may put on a layer of its own
|
|
276
|
+
* — what `sprites()` answers with (docs/extending.md "Parts of your drawing
|
|
277
|
+
* on layers of their own").
|
|
278
|
+
*/
|
|
279
|
+
export interface Sprite {
|
|
280
|
+
/** The same across frames for the same part. */
|
|
281
|
+
key: string;
|
|
282
|
+
/** Its box, untransformed: device pixels, window coordinates, like
|
|
283
|
+
* `abs`. */
|
|
284
|
+
rect: Rect;
|
|
285
|
+
/** What it draws, untransformed, when that is more than `rect`. */
|
|
286
|
+
reach?: Rect;
|
|
287
|
+
/** Draws the part where it is in the window, as `paint` would, but at
|
|
288
|
+
* opacity 1 and untransformed. */
|
|
289
|
+
paint(ctx: Context2D): void;
|
|
290
|
+
/** Compared with `===` from frame to frame; a change paints the part
|
|
291
|
+
* again. Left out, it is painted once. */
|
|
292
|
+
version?: unknown;
|
|
293
|
+
/** What the layer shows when no animation runs on it; 1 when left out. */
|
|
294
|
+
opacity?: number;
|
|
295
|
+
/** …and the transform, about `origin`; the identity when left out. */
|
|
296
|
+
transform?: SpriteMatrix;
|
|
297
|
+
/** Device pixels, window coordinates; `rect`'s centre when left out. */
|
|
298
|
+
origin?: { x: number; y: number };
|
|
299
|
+
animations?: SpriteAnimation[];
|
|
300
|
+
}
|
|
301
|
+
|
|
233
302
|
/** How an axis is bounded when layout asks an element for its size.
|
|
234
303
|
* `'exactly'` — the style decided this axis; `'at-most'` — that many pixels
|
|
235
304
|
* are on offer; `'unconstrained'` — nothing bounds it. */
|
|
@@ -570,6 +639,28 @@ export declare class Node {
|
|
|
570
639
|
* read-only.
|
|
571
640
|
*/
|
|
572
641
|
paintDamage(): Rect | null;
|
|
642
|
+
/**
|
|
643
|
+
* The parts of this element's drawing a presenter may put on layers of
|
|
644
|
+
* their own, and animate there in the render server — or null, the
|
|
645
|
+
* default. Asked every frame, after layout and before the damage is
|
|
646
|
+
* taken, by the macOS surface presenter's layer promotion and by nothing
|
|
647
|
+
* else: on X11, on Wayland and wherever a frame's scene refuses a part,
|
|
648
|
+
* the element draws everything, as it would without this.
|
|
649
|
+
*/
|
|
650
|
+
sprites(): Sprite[] | null;
|
|
651
|
+
/**
|
|
652
|
+
* The keys of this element's sprites that are on layers of their own now.
|
|
653
|
+
* Called in the frame that changes the set, before it paints; `paint`
|
|
654
|
+
* leaves a lifted part out from then on, and draws again one the set no
|
|
655
|
+
* longer holds.
|
|
656
|
+
*/
|
|
657
|
+
spritesLifted(keys: ReadonlySet<string>): void;
|
|
658
|
+
/** The render server is done with animation `id` on sprite `key`: it ran
|
|
659
|
+
* out (`finished`), or it was taken off with its layer. */
|
|
660
|
+
spriteAnimationEnded(key: string, id: string, finished: boolean): void;
|
|
661
|
+
/** Ask for the frame in which `sprites()` is asked again, when the parts
|
|
662
|
+
* changed and nothing the element draws did. A no-op where nothing asks. */
|
|
663
|
+
spritesChanged(): void;
|
|
573
664
|
/**
|
|
574
665
|
* "The pixels in `rect` moved by (dx, dy); the rest of it is new" — for an
|
|
575
666
|
* element with a viewport of its own, whose pan is a scroll in every way
|
package/src/nodes/node.js
CHANGED
|
@@ -45,6 +45,7 @@ import { NodePosition } from './position.js';
|
|
|
45
45
|
import { NodeQueries } from './queries.js';
|
|
46
46
|
import { NodeScrollBlit } from './scrollblit.js';
|
|
47
47
|
import { NodeSelectable } from './selectable.js';
|
|
48
|
+
import { NodeSprites } from './sprites.js';
|
|
48
49
|
import { NodeStyling } from './styling.js';
|
|
49
50
|
import { DEV } from './util.js';
|
|
50
51
|
|
|
@@ -608,6 +609,7 @@ export class Node {
|
|
|
608
609
|
this.root?._animating.delete(this);
|
|
609
610
|
this.root?._opaqueNodes?.delete(this);
|
|
610
611
|
this.root?._viewportFixedNodes?.delete(this);
|
|
612
|
+
this.root?._spriteNodes?.delete(this);
|
|
611
613
|
// a surface that goes away takes its selection with it, and the app-wide
|
|
612
614
|
// claim on being the one showing one goes with it too
|
|
613
615
|
this._textSelection?.destroy();
|
|
@@ -627,14 +629,18 @@ export class Node {
|
|
|
627
629
|
root?._layoutHosts?.add(this);
|
|
628
630
|
}
|
|
629
631
|
// an element answering `opaqueRect()` is one the window asks per pass,
|
|
630
|
-
//
|
|
632
|
+
// one answering `viewportFixedRects()` one it asks per scroll blit, and
|
|
633
|
+
// one answering `sprites()` one its presenter asks per frame
|
|
631
634
|
const opaque = this.opaqueRect !== Node.prototype.opaqueRect;
|
|
632
635
|
const fixed = this.viewportFixedRects !== Node.prototype.viewportFixedRects;
|
|
636
|
+
const sprites = this.sprites !== Node.prototype.sprites;
|
|
633
637
|
if (opaque) this.root?._opaqueNodes?.delete(this);
|
|
634
638
|
if (fixed) this.root?._viewportFixedNodes?.delete(this);
|
|
639
|
+
if (sprites) this.root?._spriteNodes?.delete(this);
|
|
635
640
|
this.root = root;
|
|
636
641
|
if (opaque) root?._opaqueNodes?.add(this);
|
|
637
642
|
if (fixed) root?._viewportFixedNodes?.add(this);
|
|
643
|
+
if (sprites) root?._spriteNodes?.add(this);
|
|
638
644
|
// styled before it had a window, so this is where a placed node is
|
|
639
645
|
// first registered (WindowNode._placeNodes)
|
|
640
646
|
if (this.style && isPlaced(this.style)) root?._placedNodes?.add(this);
|
|
@@ -1034,4 +1040,5 @@ installMethods(
|
|
|
1034
1040
|
NodePaint,
|
|
1035
1041
|
NodeBoxPaint,
|
|
1036
1042
|
NodeSelectable,
|
|
1043
|
+
NodeSprites,
|
|
1037
1044
|
);
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
XK_END,
|
|
17
17
|
XK_SPACE,
|
|
18
18
|
} from '../keysyms.js';
|
|
19
|
-
import { DAMAGE_SLOP, layoutDiff } from './damage.js';
|
|
19
|
+
import { DAMAGE_SLOP, NO_DAMAGE, layoutDiff } from './damage.js';
|
|
20
20
|
import { describeSize, laidBox, offsetInParent } from './layout.js';
|
|
21
21
|
import { insetRect, intersectRects, rectsOverlap } from './rects.js';
|
|
22
22
|
import {
|
|
@@ -340,11 +340,23 @@ export const Scrollable = (Base) =>
|
|
|
340
340
|
const askedY = this.scrollY;
|
|
341
341
|
this.scrollY = clampScroll(this.scrollY, this._maxScroll('y'));
|
|
342
342
|
this.scrollX = clampScroll(this.scrollX, this._maxScroll('x'));
|
|
343
|
-
// Of those three moves the clamp's is the one
|
|
344
|
-
//
|
|
343
|
+
// Of those three moves the clamp's is the one nobody asked for, and
|
|
344
|
+
// where no blit is armed the diff below claims it
|
|
345
345
|
const clamped = this.scrollX !== askedX || this.scrollY !== askedY;
|
|
346
346
|
this._reportViewport();
|
|
347
347
|
this._reportScrollTo(from);
|
|
348
|
+
// …and whichever of them moved the offsets claims the viewport the
|
|
349
|
+
// content moved in, once the pass is over. A `scrollIntoView` asks for
|
|
350
|
+
// its pass claiming nothing (issue #813), so this is the only claim
|
|
351
|
+
// its move gets, and the blit it armed reads it as the scroll's own,
|
|
352
|
+
// as it reads the one `scrollTo` makes. Not through the diff, where a
|
|
353
|
+
// claim reads as content laid out anew, which is not a pure scroll
|
|
354
|
+
// and declines that blit. A clamp under an armed blit owes it too:
|
|
355
|
+
// the walk below gives that frame's moves to the blit's ledger, and
|
|
356
|
+
// without a scrollTo nothing else has claimed the viewport.
|
|
357
|
+
if (this.scrollX !== from.x || this.scrollY !== from.y) {
|
|
358
|
+
this.root?._reflowed.add(this);
|
|
359
|
+
}
|
|
348
360
|
// `scrollX` is how far the content has moved **from its start**, which
|
|
349
361
|
// is the right-hand edge in RTL — so scrolling shifts the children the
|
|
350
362
|
// other way. Keeping it a distance rather than a coordinate is what
|
|
@@ -588,43 +600,7 @@ export const Scrollable = (Base) =>
|
|
|
588
600
|
// A pane no pass has placed arms nothing: its first pass is a layout
|
|
589
601
|
// change rather than a pure scroll.
|
|
590
602
|
if (root && this._childOrigin != null) {
|
|
591
|
-
|
|
592
|
-
// claim recorded below coalesces earlier claims into itself
|
|
593
|
-
// (addDamageRect keeps the list disjoint), after which a change
|
|
594
|
-
// inside the viewport is indistinguishable from the scroll's own
|
|
595
|
-
// claim — the blind spot the claim-time cancel in
|
|
596
|
-
// WindowNode.invalidate cannot cover (react-x11#295). The scroll
|
|
597
|
-
// has not claimed yet, so damage already overlapping this viewport
|
|
598
|
-
// is foreign by construction: poison the frame instead of arming,
|
|
599
|
-
// and the full-viewport repaint below stays in force.
|
|
600
|
-
const arming = this._pendingBlitFrom == null;
|
|
601
|
-
// The ledger this frame's changes inside the viewport are written
|
|
602
|
-
// to (issue #398). Opened with the blit and read by
|
|
603
|
-
// _applyScrollBlits, which clears it beside the origin.
|
|
604
|
-
if (arming) this._blitLedger = [];
|
|
605
|
-
if (arming && Array.isArray(root._damage)) {
|
|
606
|
-
const zone = insetRect(this.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
607
|
-
for (const rect of root._damage) {
|
|
608
|
-
// Already coalesced, so these rects are as coarse as the frame
|
|
609
|
-
// has made them — which the ledger reads conservatively: a blob
|
|
610
|
-
// that swallowed the viewport says so and poisons, exactly as
|
|
611
|
-
// this gate used to for every claim it saw.
|
|
612
|
-
if (rectsOverlap(rect, zone) && !this._recordBlitClaim(rect)) {
|
|
613
|
-
this._pendingBlitFrom = BLIT_POISONED;
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
// An element that also shifted its own drawing this frame
|
|
619
|
-
// (`scrollContents`, issue #303) is two shifts of the same pixels,
|
|
620
|
-
// and a frame can only have one.
|
|
621
|
-
if (this._pendingBlitContents) this._pendingBlitFrom = BLIT_POISONED;
|
|
622
|
-
// The offsets whose pixels are on screen, captured before the first
|
|
623
|
-
// change of the frame: the frame's blit fast path (issue #138) shifts
|
|
624
|
-
// from *these* to wherever layout settles, however many scrollTo
|
|
625
|
-
// calls land in between.
|
|
626
|
-
this._pendingBlitFrom ??= { x: this.scrollX, y: this.scrollY };
|
|
627
|
-
(root._pendingScrolls ??= new Set()).add(this);
|
|
603
|
+
this._armScrollBlit(root);
|
|
628
604
|
// ... and the claim about to be recorded is the scroll itself, not a
|
|
629
605
|
// reason to un-blit it
|
|
630
606
|
root._scrollClaim = this;
|
|
@@ -644,6 +620,52 @@ export const Scrollable = (Base) =>
|
|
|
644
620
|
if (root) root._scrollClaim = null;
|
|
645
621
|
}
|
|
646
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Arm the frame's scroll blit (issue #138) for a move of this pane's
|
|
625
|
+
* offsets: a `scrollTo` about to make one, or a `scrollIntoView` whose
|
|
626
|
+
* pass may (`scrollIntoView`). Before the move, and before the claim
|
|
627
|
+
* that comes with it.
|
|
628
|
+
*
|
|
629
|
+
* Arming is the one moment the evidence still exists: the viewport
|
|
630
|
+
* claim coalesces earlier claims into itself (addDamageRect keeps the
|
|
631
|
+
* list disjoint), after which a change inside the viewport is
|
|
632
|
+
* indistinguishable from the scroll's own claim — the blind spot the
|
|
633
|
+
* claim-time cancel in WindowNode.invalidate cannot cover
|
|
634
|
+
* (react-x11#295). The scroll has not claimed yet, so damage already
|
|
635
|
+
* overlapping this viewport is foreign by construction: poison the
|
|
636
|
+
* frame instead of arming, and the full-viewport repaint stays in force.
|
|
637
|
+
*/
|
|
638
|
+
_armScrollBlit(root) {
|
|
639
|
+
const arming = this._pendingBlitFrom == null;
|
|
640
|
+
// The ledger this frame's changes inside the viewport are written
|
|
641
|
+
// to (issue #398). Opened with the blit and read by
|
|
642
|
+
// _applyScrollBlits, which clears it beside the origin.
|
|
643
|
+
if (arming) this._blitLedger = [];
|
|
644
|
+
if (arming && Array.isArray(root._damage)) {
|
|
645
|
+
const zone = insetRect(this.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
646
|
+
for (const rect of root._damage) {
|
|
647
|
+
// Already coalesced, so these rects are as coarse as the frame
|
|
648
|
+
// has made them — which the ledger reads conservatively: a blob
|
|
649
|
+
// that swallowed the viewport says so and poisons, exactly as
|
|
650
|
+
// this gate used to for every claim it saw.
|
|
651
|
+
if (rectsOverlap(rect, zone) && !this._recordBlitClaim(rect)) {
|
|
652
|
+
this._pendingBlitFrom = BLIT_POISONED;
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// An element that also shifted its own drawing this frame
|
|
658
|
+
// (`scrollContents`, issue #303) is two shifts of the same pixels,
|
|
659
|
+
// and a frame can only have one.
|
|
660
|
+
if (this._pendingBlitContents) this._pendingBlitFrom = BLIT_POISONED;
|
|
661
|
+
// The offsets whose pixels are on screen, captured before the first
|
|
662
|
+
// change of the frame: the frame's blit fast path (issue #138) shifts
|
|
663
|
+
// from *these* to wherever layout settles, however many scrollTo
|
|
664
|
+
// calls land in between.
|
|
665
|
+
this._pendingBlitFrom ??= { x: this.scrollX, y: this.scrollY };
|
|
666
|
+
(root._pendingScrolls ??= new Set()).add(this);
|
|
667
|
+
}
|
|
668
|
+
|
|
647
669
|
/**
|
|
648
670
|
* Keep the part of a request the extent in hand cannot answer, for the
|
|
649
671
|
* pass that will measure one that can. `_scrollToDevice` clamps against
|
|
@@ -864,13 +886,29 @@ export const Scrollable = (Base) =>
|
|
|
864
886
|
* that has no geometry yet. `absolutize` resolves it against freshly
|
|
865
887
|
* computed yoga positions, and `onScroll` reports the move once that
|
|
866
888
|
* pass is over (`_reportScrollTo`).
|
|
889
|
+
*
|
|
890
|
+
* Asking claims nothing (issue #813). Most of the time the node is in
|
|
891
|
+
* view already — every Tab between the fields of a form in a pane — and
|
|
892
|
+
* a claim made now, before anyone knows, repainted the pane's whole
|
|
893
|
+
* viewport for a frame that moved nothing. So the pass is asked for
|
|
894
|
+
* with no damage, and a request that finds its node in view costs what
|
|
895
|
+
* the focus change costs outside a pane. One that does move the pane
|
|
896
|
+
* moves it the way a scroll does: the blit is armed here, as `scrollTo`
|
|
897
|
+
* arms it, and the pass claims the viewport once the offsets have
|
|
898
|
+
* changed (`_absolutizeChildren`), which `_applyScrollBlits` narrows to
|
|
899
|
+
* the strip the move uncovered.
|
|
867
900
|
*/
|
|
868
901
|
scrollIntoView(node) {
|
|
869
902
|
if (!node || !this.isScroller()) return;
|
|
870
903
|
this._scrollIntoViewTarget = node;
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
904
|
+
const root = this.root;
|
|
905
|
+
if (!root) return;
|
|
906
|
+
// a pane no pass has placed arms nothing, as for `scrollTo`
|
|
907
|
+
if (this._childOrigin != null) this._armScrollBlit(root);
|
|
908
|
+
// `'scroll'`: the walk goes everywhere, which is what reaches this
|
|
909
|
+
// pane however clean the boxes above it are, and the content floors
|
|
910
|
+
// owe nothing
|
|
911
|
+
root.invalidate(true, NO_DAMAGE, 'scroll', this);
|
|
874
912
|
}
|
|
875
913
|
|
|
876
914
|
_resolveScrollIntoView() {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Sprites (issue #819): the parts of what an element draws that a retained
|
|
2
|
+
// presenter may lift onto layers of their own, and animate there in the
|
|
3
|
+
// render server. The element's half of the seam is here; the presenter's is
|
|
4
|
+
// src/cocoa/sprites.js, under layer promotion.
|
|
5
|
+
//
|
|
6
|
+
// It is the shape of `opaqueRect()`: an accessor core asks every frame, a
|
|
7
|
+
// registry of the nodes that override it (`WindowNode._spriteNodes`, kept by
|
|
8
|
+
// `_setRoot`), and a default that answers nothing, so an element that never
|
|
9
|
+
// heard of sprites pays nothing and one that answers them draws exactly what
|
|
10
|
+
// it draws today wherever nothing asks — on X11, on Wayland, on the layer
|
|
11
|
+
// presenter, and in any frame whose scene refuses a lift.
|
|
12
|
+
|
|
13
|
+
/** Node's half of sprites, installed onto `Node.prototype` by node.js. */
|
|
14
|
+
export class NodeSprites {
|
|
15
|
+
/**
|
|
16
|
+
* The parts of this element's drawing a presenter may put on layers of
|
|
17
|
+
* their own, or null, the default. Each is a plain object:
|
|
18
|
+
*
|
|
19
|
+
* - `key`: a string, the same across frames for the same part.
|
|
20
|
+
* - `rect`: its box, untransformed — device pixels, window coordinates,
|
|
21
|
+
* like `abs`.
|
|
22
|
+
* - `reach`: what it draws, untransformed, when that is more than `rect`.
|
|
23
|
+
* - `paint(ctx)`: draws it where it is in the window, as `paint` would,
|
|
24
|
+
* but at opacity 1 and untransformed: the layer carries both.
|
|
25
|
+
* - `version`: compared with `===` from frame to frame; a change paints
|
|
26
|
+
* the part again. Left out, it is painted once.
|
|
27
|
+
* - `opacity`, and `transform` — CSS's `matrix(a, b, c, d, e, f)` as an
|
|
28
|
+
* array, `e` and `f` in device pixels — about `origin`, a point in
|
|
29
|
+
* device pixels that defaults to `rect`'s centre: the values the layer
|
|
30
|
+
* shows when no animation is running on it.
|
|
31
|
+
* - `animations`: what the render server runs on it, each `{ id,
|
|
32
|
+
* property: 'opacity' | 'transform', values, keyTimes, timings,
|
|
33
|
+
* duration, delay, repeat, autoreverse, hold }`, with times in
|
|
34
|
+
* milliseconds and `delay` counted from the frame that asks — negative
|
|
35
|
+
* for one that started before it. An animation is attached once per
|
|
36
|
+
* `id`: to change one, give it a new id.
|
|
37
|
+
*
|
|
38
|
+
* Asked every frame, after layout and before the damage is taken, on a
|
|
39
|
+
* window whose presenter lifts sprites; nowhere else. An element whose
|
|
40
|
+
* parts changed while nothing was going to paint asks for that frame with
|
|
41
|
+
* `spritesChanged()`.
|
|
42
|
+
*/
|
|
43
|
+
sprites() {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The keys of this element's sprites that are on layers of their own now,
|
|
49
|
+
* a set the element may keep. Called in the frame that changes it, before
|
|
50
|
+
* that frame paints. From then on `paint` must not draw a lifted part —
|
|
51
|
+
* where it is, the window's bitmap holds a hole the layer shows through —
|
|
52
|
+
* and must draw again one the set no longer holds: a presenter gives a
|
|
53
|
+
* part back whenever the scene stops allowing it, and the bitmap under it
|
|
54
|
+
* is repainted in that same frame.
|
|
55
|
+
*/
|
|
56
|
+
spritesLifted(keys) {}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The render server is done with animation `id` on sprite `key`: it ran
|
|
60
|
+
* out (`finished`), or it was taken off with its layer. Called from the
|
|
61
|
+
* backend's event, between frames.
|
|
62
|
+
*/
|
|
63
|
+
spriteAnimationEnded(key, id, finished) {}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ask for the frame in which this element's sprites are asked for again —
|
|
67
|
+
* one whose parts changed while nothing it drew had. Does nothing on a
|
|
68
|
+
* window whose presenter lifts none.
|
|
69
|
+
*/
|
|
70
|
+
spritesChanged() {
|
|
71
|
+
this.root?.window?.spritesChanged?.(this);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -196,6 +196,9 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
196
196
|
// the nodes answering `viewportFixedRects()`, whose rects a scroll pane
|
|
197
197
|
// they are in repairs when it blits (`_viewportFixedPins`)
|
|
198
198
|
this._viewportFixedNodes = new Set();
|
|
199
|
+
// the nodes answering `sprites()`, which a presenter that lifts parts of
|
|
200
|
+
// an element onto layers asks every frame (src/cocoa/sprites.js)
|
|
201
|
+
this._spriteNodes = new Set();
|
|
199
202
|
// Nodes that want the `attention` event (ntk#37) — an
|
|
200
203
|
// `unstable_onAttention` prop,
|
|
201
204
|
// an `:attention` block, or both. Built before the EventManager so the
|