react-x11 2.28.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/promotion.js +58 -13
- 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/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/promotion.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
propBoxProps,
|
|
46
46
|
stylePaintsPlain,
|
|
47
47
|
} from './presenter.js';
|
|
48
|
+
import { SpriteLayers } from './sprites.js';
|
|
48
49
|
|
|
49
50
|
const ORIGIN = Object.freeze({ x: 0, y: 0 });
|
|
50
51
|
|
|
@@ -61,6 +62,17 @@ const IDLE_GRACE_MS = 1000;
|
|
|
61
62
|
// the repaint they save (the layer presenter's rule, per raster).
|
|
62
63
|
const MAX_DIRTY_RECTS = 16;
|
|
63
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
|
+
|
|
64
76
|
const rectsOverlap = (a, b) =>
|
|
65
77
|
a.x < b.x + b.width &&
|
|
66
78
|
b.x < a.x + a.width &&
|
|
@@ -243,6 +255,9 @@ export class CocoaPromotion {
|
|
|
243
255
|
layerOf: (node) => this.promoted.get(node)?.visual.layer ?? null,
|
|
244
256
|
onIdle: (node) => this._idle(node),
|
|
245
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);
|
|
246
261
|
this._claiming = false;
|
|
247
262
|
}
|
|
248
263
|
|
|
@@ -390,8 +405,19 @@ export class CocoaPromotion {
|
|
|
390
405
|
*/
|
|
391
406
|
frame(root, layoutRan) {
|
|
392
407
|
if (layoutRan) this.layoutGen++;
|
|
393
|
-
|
|
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
|
+
}
|
|
394
419
|
const native = this.native;
|
|
420
|
+
let lifted = null;
|
|
395
421
|
native.txBegin({ disableActions: true });
|
|
396
422
|
try {
|
|
397
423
|
// Later-painted first: a node painted after this one that is coming
|
|
@@ -416,13 +442,26 @@ export class CocoaPromotion {
|
|
|
416
442
|
}
|
|
417
443
|
}
|
|
418
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.
|
|
419
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);
|
|
420
454
|
for (let i = 0; i < order.length; i++) {
|
|
421
|
-
|
|
455
|
+
const { node, state } = order[i];
|
|
456
|
+
if (node) this._sync(node, i, root, layoutRan);
|
|
457
|
+
else this.sprites.sync(state, i);
|
|
422
458
|
}
|
|
423
459
|
} finally {
|
|
424
460
|
native.txCommit();
|
|
425
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);
|
|
426
465
|
}
|
|
427
466
|
|
|
428
467
|
_mayStay(node, key) {
|
|
@@ -528,17 +567,22 @@ export class CocoaPromotion {
|
|
|
528
567
|
|
|
529
568
|
_inPaintOrder(nodes) {
|
|
530
569
|
const keyed = nodes.map((node) => ({ node, key: this._paintKey(node) }));
|
|
531
|
-
keyed.sort(
|
|
532
|
-
if (!a.key || !b.key) return (a.key ? 1 : 0) - (b.key ? 1 : 0);
|
|
533
|
-
const n = Math.min(a.key.length, b.key.length);
|
|
534
|
-
for (let i = 0; i < n; i++) {
|
|
535
|
-
if (a.key[i] !== b.key[i]) return a.key[i] - b.key[i];
|
|
536
|
-
}
|
|
537
|
-
return a.key.length - b.key.length;
|
|
538
|
-
});
|
|
570
|
+
keyed.sort(byPaintKey);
|
|
539
571
|
return keyed;
|
|
540
572
|
}
|
|
541
573
|
|
|
574
|
+
/**
|
|
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
|
+
|
|
542
586
|
/**
|
|
543
587
|
* Is nothing painted over this node, is all of it visible, and is it in
|
|
544
588
|
* no faded group? Walked up the chain: at every level, what the parent
|
|
@@ -548,11 +592,11 @@ export class CocoaPromotion {
|
|
|
548
592
|
* it, and the parent must not fade: its group is drawn on the bitmap or on
|
|
549
593
|
* its own layer, and either way a layer of the node's would be outside it.
|
|
550
594
|
*/
|
|
551
|
-
_clear(node) {
|
|
595
|
+
_clear(node, bounds = node._subtreeBounds()) {
|
|
552
596
|
// the exact reach, not `paintBounds()`: that one carries the damage
|
|
553
597
|
// model's pixel of slop, and a section laid out flush under a card
|
|
554
|
-
// would read as reaching into it
|
|
555
|
-
|
|
598
|
+
// would read as reaching into it. An element's part is asked about at
|
|
599
|
+
// everywhere it can draw (src/cocoa/sprites.js).
|
|
556
600
|
for (let n = node; !n.isWindow; n = n.parent) {
|
|
557
601
|
const parent = n.parent;
|
|
558
602
|
if (!parent) return false;
|
|
@@ -733,6 +777,7 @@ export class CocoaPromotion {
|
|
|
733
777
|
|
|
734
778
|
/** Everything off the root layer and freed: the window is going. */
|
|
735
779
|
destroy() {
|
|
780
|
+
this.sprites.destroy();
|
|
736
781
|
for (const timer of this.idle.values()) clearTimeout(timer);
|
|
737
782
|
this.idle.clear();
|
|
738
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
|
);
|
|
@@ -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
|