react-x11 2.28.0 → 2.30.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/app.js +157 -9
- package/src/cocoa/panehost.js +226 -2
- package/src/cocoa/panewindow.js +204 -14
- package/src/cocoa/promotion.js +78 -13
- package/src/cocoa/sprites.js +605 -0
- package/src/cocoa/window.js +4 -0
- package/src/components/Select.js +98 -7
- package/src/frame/index.js +60 -7
- package/src/node.d.ts +96 -0
- package/src/nodes/node.js +8 -1
- package/src/nodes/sprites.js +73 -0
- package/src/nodes/window/window.js +14 -1
- package/src/types/components.d.ts +10 -0
|
@@ -0,0 +1,605 @@
|
|
|
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 { intersectRects } from '../nodes/rects.js';
|
|
29
|
+
import { DEV } from '../nodes/util.js';
|
|
30
|
+
|
|
31
|
+
// Past this many device pixels on a side a part is drawn by its element: a
|
|
32
|
+
// layer's contents are a bitmap, and a page-sized one is a page-sized
|
|
33
|
+
// upload for every new version.
|
|
34
|
+
const MAX_SIDE = 8192;
|
|
35
|
+
|
|
36
|
+
// Sorted after the element's children, which its own drawing comes after
|
|
37
|
+
// (`Node.paint` paints the children first), and before its next sibling.
|
|
38
|
+
const AFTER_CHILDREN = Number.MAX_SAFE_INTEGER;
|
|
39
|
+
|
|
40
|
+
const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
41
|
+
|
|
42
|
+
// what a part's raster holds before its first paint
|
|
43
|
+
const UNPAINTED = Symbol('unpainted');
|
|
44
|
+
|
|
45
|
+
// the ids the bridge reports an animation's end under: unique per process,
|
|
46
|
+
// looked up per app (`_animationEnds`), like LayerAnimations'
|
|
47
|
+
let spriteSeq = 0;
|
|
48
|
+
|
|
49
|
+
const finite = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
50
|
+
|
|
51
|
+
const isRect = (r) =>
|
|
52
|
+
r != null &&
|
|
53
|
+
finite(r.x) &&
|
|
54
|
+
finite(r.y) &&
|
|
55
|
+
finite(r.width) &&
|
|
56
|
+
finite(r.height) &&
|
|
57
|
+
r.width > 0 &&
|
|
58
|
+
r.height > 0;
|
|
59
|
+
|
|
60
|
+
const isMatrix = (m) => Array.isArray(m) && m.length === 6 && m.every(finite);
|
|
61
|
+
|
|
62
|
+
const isCurve = (t) =>
|
|
63
|
+
typeof t === 'string' ||
|
|
64
|
+
(Array.isArray(t) &&
|
|
65
|
+
t.length === 4 &&
|
|
66
|
+
t.every(finite) &&
|
|
67
|
+
t[0] >= 0 &&
|
|
68
|
+
t[0] <= 1 &&
|
|
69
|
+
t[2] >= 0 &&
|
|
70
|
+
t[2] <= 1);
|
|
71
|
+
|
|
72
|
+
/** Is `a` an animation the bridge will take as it is — so that a bad one
|
|
73
|
+
* is refused here rather than thrown from inside a frame? */
|
|
74
|
+
function isAnimation(a) {
|
|
75
|
+
if (a == null || typeof a.id !== 'string') return false;
|
|
76
|
+
if (a.property !== 'opacity' && a.property !== 'transform') return false;
|
|
77
|
+
if (!finite(a.duration) || !(a.duration > 0)) return false;
|
|
78
|
+
const values = a.values;
|
|
79
|
+
if (!Array.isArray(values) || values.length < 2) return false;
|
|
80
|
+
const value = a.property === 'opacity' ? finite : isMatrix;
|
|
81
|
+
if (!values.every(value)) return false;
|
|
82
|
+
if (a.keyTimes !== undefined) {
|
|
83
|
+
const t = a.keyTimes;
|
|
84
|
+
if (!Array.isArray(t) || t.length !== values.length || t[0] !== 0) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < t.length; i++) {
|
|
88
|
+
if (!finite(t[i]) || t[i] > 1 || (i > 0 && t[i] < t[i - 1])) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (a.timings !== undefined) {
|
|
94
|
+
const t = a.timings;
|
|
95
|
+
if (!Array.isArray(t) || t.length !== values.length - 1) return false;
|
|
96
|
+
if (!t.every(isCurve)) return false;
|
|
97
|
+
}
|
|
98
|
+
if (a.delay !== undefined && !finite(a.delay)) return false;
|
|
99
|
+
if (
|
|
100
|
+
a.repeat !== undefined &&
|
|
101
|
+
!(a.repeat === Infinity || (finite(a.repeat) && a.repeat > 0))
|
|
102
|
+
) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Is `sprite` a part as `Node.sprites()` documents one? */
|
|
109
|
+
function isSprite(sprite) {
|
|
110
|
+
if (sprite == null || typeof sprite.key !== 'string') return false;
|
|
111
|
+
if (!isRect(sprite.rect) || typeof sprite.paint !== 'function') return false;
|
|
112
|
+
if (sprite.reach !== undefined && !isRect(sprite.reach)) return false;
|
|
113
|
+
if (sprite.clip !== undefined && !isRect(sprite.clip)) return false;
|
|
114
|
+
if (sprite.opacity !== undefined && !finite(sprite.opacity)) return false;
|
|
115
|
+
if (sprite.transform !== undefined && !isMatrix(sprite.transform)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
sprite.origin !== undefined &&
|
|
120
|
+
!(
|
|
121
|
+
sprite.origin != null &&
|
|
122
|
+
finite(sprite.origin.x) &&
|
|
123
|
+
finite(sprite.origin.y)
|
|
124
|
+
)
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const animations = sprite.animations;
|
|
129
|
+
if (animations !== undefined) {
|
|
130
|
+
if (!Array.isArray(animations) || !animations.every(isAnimation)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const originOf = (sprite) =>
|
|
138
|
+
sprite.origin ?? {
|
|
139
|
+
x: sprite.rect.x + sprite.rect.width / 2,
|
|
140
|
+
y: sprite.rect.y + sprite.rect.height / 2,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** `rect` through `m` about `origin`: the bounds of its four corners. */
|
|
144
|
+
function mapRect(rect, m, origin) {
|
|
145
|
+
let x0 = Infinity;
|
|
146
|
+
let y0 = Infinity;
|
|
147
|
+
let x1 = -Infinity;
|
|
148
|
+
let y1 = -Infinity;
|
|
149
|
+
for (const [px, py] of [
|
|
150
|
+
[rect.x, rect.y],
|
|
151
|
+
[rect.x + rect.width, rect.y],
|
|
152
|
+
[rect.x, rect.y + rect.height],
|
|
153
|
+
[rect.x + rect.width, rect.y + rect.height],
|
|
154
|
+
]) {
|
|
155
|
+
const dx = px - origin.x;
|
|
156
|
+
const dy = py - origin.y;
|
|
157
|
+
const x = m[0] * dx + m[2] * dy + m[4] + origin.x;
|
|
158
|
+
const y = m[1] * dx + m[3] * dy + m[5] + origin.y;
|
|
159
|
+
if (x < x0) x0 = x;
|
|
160
|
+
if (y < y0) y0 = y;
|
|
161
|
+
if (x > x1) x1 = x;
|
|
162
|
+
if (y > y1) y1 = y;
|
|
163
|
+
}
|
|
164
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function unionRect(a, b) {
|
|
168
|
+
if (!a) return b;
|
|
169
|
+
const x = Math.min(a.x, b.x);
|
|
170
|
+
const y = Math.min(a.y, b.y);
|
|
171
|
+
return {
|
|
172
|
+
x,
|
|
173
|
+
y,
|
|
174
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
175
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Everywhere `sprite` can draw while its animations run: its reach through
|
|
181
|
+
* its own transform and every transform it is animated through, out to
|
|
182
|
+
* whole pixels and the antialiasing a raster is padded for. Between two
|
|
183
|
+
* keyframes Core Animation interpolates the matrices by their parts, which
|
|
184
|
+
* stays within a pixel of the two for the densely sampled keyframes a CSS
|
|
185
|
+
* transform arrives as; an element whose keyframes are far apart, or whose
|
|
186
|
+
* curve overshoots, offers a `reach` that covers what is between them.
|
|
187
|
+
*/
|
|
188
|
+
function extentOf(sprite) {
|
|
189
|
+
const reach = sprite.reach ?? sprite.rect;
|
|
190
|
+
const origin = originOf(sprite);
|
|
191
|
+
let box = mapRect(reach, sprite.transform ?? IDENTITY, origin);
|
|
192
|
+
for (const a of sprite.animations ?? []) {
|
|
193
|
+
if (a.property !== 'transform') continue;
|
|
194
|
+
for (const m of a.values) box = unionRect(box, mapRect(reach, m, origin));
|
|
195
|
+
}
|
|
196
|
+
const x = Math.floor(box.x) - RASTER_PAD;
|
|
197
|
+
const y = Math.floor(box.y) - RASTER_PAD;
|
|
198
|
+
return {
|
|
199
|
+
x,
|
|
200
|
+
y,
|
|
201
|
+
width: Math.ceil(box.x + box.width) + RASTER_PAD - x,
|
|
202
|
+
height: Math.ceil(box.y + box.height) + RASTER_PAD - y,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The raster a part's content is painted into: its reach, out to whole
|
|
207
|
+
* pixels and padded for antialiasing, untransformed. */
|
|
208
|
+
function rasterRectOf(sprite) {
|
|
209
|
+
const reach = sprite.reach ?? sprite.rect;
|
|
210
|
+
const x = Math.floor(reach.x) - RASTER_PAD;
|
|
211
|
+
const y = Math.floor(reach.y) - RASTER_PAD;
|
|
212
|
+
return {
|
|
213
|
+
x,
|
|
214
|
+
y,
|
|
215
|
+
width: Math.ceil(reach.x + reach.width) + RASTER_PAD - x,
|
|
216
|
+
height: Math.ceil(reach.y + reach.height) + RASTER_PAD - y,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const sameMatrix = (a, b) =>
|
|
221
|
+
a === b || (a != null && b != null && a.every((v, i) => v === b[i]));
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The lifted parts of every element in one window: a layer each, its raster,
|
|
225
|
+
* and what the render server runs on it. `CocoaPromotion` asks, decides and
|
|
226
|
+
* syncs through it inside its own frame.
|
|
227
|
+
*/
|
|
228
|
+
export class SpriteLayers {
|
|
229
|
+
constructor(promotion) {
|
|
230
|
+
this.promotion = promotion;
|
|
231
|
+
this.native = promotion.native;
|
|
232
|
+
this.scale = promotion.scale;
|
|
233
|
+
this.app = promotion.app;
|
|
234
|
+
// a matrix and a negative delay are what a CSS animation needs from the
|
|
235
|
+
// bridge, and @windowkit/appkit 0.19 is where both are
|
|
236
|
+
this.supported = Boolean(
|
|
237
|
+
this.native.transformForms?.()?.includes?.('matrix'),
|
|
238
|
+
);
|
|
239
|
+
this.hosts = new Map(); // element -> Map(key -> state)
|
|
240
|
+
this._warned = new WeakSet();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_ends() {
|
|
244
|
+
return (this.app._animationEnds ??= new Map());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_warn(host, message) {
|
|
248
|
+
if (!DEV || this._warned.has(host)) return;
|
|
249
|
+
this._warned.add(host);
|
|
250
|
+
console.warn(`react-x11: <${host.kind}> ${message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Every element's answer this frame, before anything is decided: the
|
|
255
|
+
* elements that offer a part, and what they offer. An element that throws
|
|
256
|
+
* offers nothing and draws everything, which is what it would do anywhere
|
|
257
|
+
* else.
|
|
258
|
+
*/
|
|
259
|
+
ask(root) {
|
|
260
|
+
const offers = new Map();
|
|
261
|
+
if (!this.supported) return offers;
|
|
262
|
+
for (const host of root._spriteNodes ?? []) {
|
|
263
|
+
if (host.destroyed) continue;
|
|
264
|
+
let list = null;
|
|
265
|
+
try {
|
|
266
|
+
list = host.sprites();
|
|
267
|
+
} catch (err) {
|
|
268
|
+
this._warn(
|
|
269
|
+
host,
|
|
270
|
+
`sprites() threw, and its parts are drawn by it: ${err?.message ?? err}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (Array.isArray(list) && list.length !== 0) offers.set(host, list);
|
|
274
|
+
}
|
|
275
|
+
return offers;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Which offered parts go on layers this frame, against the scene as laid
|
|
280
|
+
* out and the layers promotion has decided on: a part is lifted, kept, or
|
|
281
|
+
* given back. Answers the elements whose set of lifted parts changed.
|
|
282
|
+
*/
|
|
283
|
+
decide(root, offers) {
|
|
284
|
+
const changed = new Set();
|
|
285
|
+
for (const [host, lifted] of this.hosts) {
|
|
286
|
+
if (offers.has(host)) continue;
|
|
287
|
+
for (const state of lifted.values()) this._drop(state, root);
|
|
288
|
+
this.hosts.delete(host);
|
|
289
|
+
if (!host.destroyed) changed.add(host);
|
|
290
|
+
}
|
|
291
|
+
for (const [host, list] of offers) {
|
|
292
|
+
const was = this.hosts.get(host);
|
|
293
|
+
const now = new Map();
|
|
294
|
+
// where the element stands in the paint order, or null where none of
|
|
295
|
+
// it may go on a layer: hidden, faded, inside a <glarea>
|
|
296
|
+
const place = this.promotion._spritePlace(host);
|
|
297
|
+
for (let index = 0; place && index < list.length; index++) {
|
|
298
|
+
const sprite = list[index];
|
|
299
|
+
if (!isSprite(sprite)) {
|
|
300
|
+
this._warn(
|
|
301
|
+
host,
|
|
302
|
+
`offered a sprite that is not one (${JSON.stringify(sprite?.key)}); see Node.sprites()`,
|
|
303
|
+
);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (now.has(sprite.key)) continue;
|
|
307
|
+
const raster = rasterRectOf(sprite);
|
|
308
|
+
if (raster.width > MAX_SIDE || raster.height > MAX_SIDE) continue;
|
|
309
|
+
// what shows of everywhere it can be: inside its own clip, and the
|
|
310
|
+
// clips of its element's ancestors, which cut its layer (`_clear`)
|
|
311
|
+
const cut = { clip: sprite.clip ?? null, bounds: null };
|
|
312
|
+
const extent = extentOf(sprite);
|
|
313
|
+
const bounds = cut.clip ? intersectRects(extent, cut.clip) : extent;
|
|
314
|
+
if (!bounds || !this.promotion._clear(host, bounds, cut)) continue;
|
|
315
|
+
const shown = cut.bounds;
|
|
316
|
+
let state = was?.get(sprite.key);
|
|
317
|
+
// a clip that comes or goes is a layer in a box or out of one: lifted
|
|
318
|
+
// again, rather than a running layer moved between parents
|
|
319
|
+
if (state && Boolean(state.clip) !== Boolean(cut.clip)) {
|
|
320
|
+
this._drop(state, root);
|
|
321
|
+
state = null;
|
|
322
|
+
}
|
|
323
|
+
const lifting = !state;
|
|
324
|
+
if (lifting) state = this._lift(host, sprite.key, cut.clip !== null);
|
|
325
|
+
state.sprite = sprite;
|
|
326
|
+
state.clip = cut.clip;
|
|
327
|
+
state.order = [...place, AFTER_CHILDREN, index];
|
|
328
|
+
// the bitmap under where it shows repaints without it, from this
|
|
329
|
+
// frame on; where it went since, it is the element's own claims
|
|
330
|
+
// that move the hole
|
|
331
|
+
if (lifting) this.promotion._claim(root, shown);
|
|
332
|
+
state.extent = shown;
|
|
333
|
+
now.set(sprite.key, state);
|
|
334
|
+
}
|
|
335
|
+
if (was) {
|
|
336
|
+
for (const [key, state] of was) {
|
|
337
|
+
if (!now.has(key)) this._drop(state, root);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (now.size !== 0) this.hosts.set(host, now);
|
|
341
|
+
else this.hosts.delete(host);
|
|
342
|
+
const before = was?.size ?? 0;
|
|
343
|
+
if (before !== now.size || [...now.keys()].some((k) => !was.has(k))) {
|
|
344
|
+
changed.add(host);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return changed;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Every lifted part, with its place in the paint order. */
|
|
351
|
+
entries() {
|
|
352
|
+
const out = [];
|
|
353
|
+
for (const lifted of this.hosts.values()) {
|
|
354
|
+
for (const state of lifted.values()) out.push(state);
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Tell each element that changed which of its parts are lifted now —
|
|
360
|
+
* after the decisions, before the frame paints. */
|
|
361
|
+
notify(changed) {
|
|
362
|
+
for (const host of changed) {
|
|
363
|
+
if (host.destroyed) continue;
|
|
364
|
+
const keys = new Set(this.hosts.get(host)?.keys() ?? []);
|
|
365
|
+
try {
|
|
366
|
+
host.spritesLifted(keys);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
this._warn(host, `spritesLifted() threw: ${err?.message ?? err}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_lift(host, key, clipped) {
|
|
374
|
+
const layer = this.native.createLayer();
|
|
375
|
+
// a part cut to a clip is in a box of the clip's size that masks to it,
|
|
376
|
+
// and the box is what stands among the layers above the bitmap
|
|
377
|
+
let box = null;
|
|
378
|
+
if (clipped) {
|
|
379
|
+
box = this.native.createLayer();
|
|
380
|
+
this.native.addSublayer(this.promotion.rootVisual.layer, box);
|
|
381
|
+
this.native.addSublayer(box, layer);
|
|
382
|
+
} else {
|
|
383
|
+
this.native.addSublayer(this.promotion.rootVisual.layer, layer);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
host,
|
|
387
|
+
key,
|
|
388
|
+
layer,
|
|
389
|
+
box,
|
|
390
|
+
boxProps: {},
|
|
391
|
+
clip: null, // device pixels, window coordinates
|
|
392
|
+
raster: new RasterState(),
|
|
393
|
+
painted: UNPAINTED,
|
|
394
|
+
rect: null, // the raster's rect, device pixels, window coordinates
|
|
395
|
+
phase: null, // where in a pixel its reach starts
|
|
396
|
+
props: {},
|
|
397
|
+
runs: new Map(), // element's animation id -> { caId, caKey, held }
|
|
398
|
+
// ids the render server is done with, while the element still lists
|
|
399
|
+
// them: an animation is attached once per id, and an end the element
|
|
400
|
+
// has not caught up with must not start it again
|
|
401
|
+
done: new Set(),
|
|
402
|
+
sprite: null,
|
|
403
|
+
extent: null,
|
|
404
|
+
order: null,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Off its layer: the layer and its raster go, its animations are
|
|
409
|
+
* forgotten, and the bitmap under where it could be repaints with the
|
|
410
|
+
* element drawing it again. */
|
|
411
|
+
_drop(state, root) {
|
|
412
|
+
this.native.removeFromSuperlayer(state.box ?? state.layer);
|
|
413
|
+
state.raster.release(this.native);
|
|
414
|
+
for (const run of state.runs.values()) this._ends().delete(run.caId);
|
|
415
|
+
state.runs.clear();
|
|
416
|
+
if (!state.host.destroyed && state.extent) {
|
|
417
|
+
this.promotion._claim(root, state.extent);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The layer as the part says, at `z` among the layers above the bitmap. */
|
|
422
|
+
sync(state, z) {
|
|
423
|
+
const sprite = state.sprite;
|
|
424
|
+
const s = this.scale;
|
|
425
|
+
const rect = rasterRectOf(sprite);
|
|
426
|
+
const reach = sprite.reach ?? sprite.rect;
|
|
427
|
+
const phase = `${reach.x - Math.floor(reach.x)},${reach.y - Math.floor(reach.y)}`;
|
|
428
|
+
// painted again for a new version, and for a raster of another size or
|
|
429
|
+
// with the part at another fraction of a pixel; a move by whole pixels
|
|
430
|
+
// is the layer's alone
|
|
431
|
+
if (
|
|
432
|
+
state.painted === UNPAINTED ||
|
|
433
|
+
sprite.version !== state.painted ||
|
|
434
|
+
state.rect?.width !== rect.width ||
|
|
435
|
+
state.rect?.height !== rect.height ||
|
|
436
|
+
state.phase !== phase
|
|
437
|
+
) {
|
|
438
|
+
this._paint(state, sprite, rect);
|
|
439
|
+
state.phase = phase;
|
|
440
|
+
}
|
|
441
|
+
state.rect = rect;
|
|
442
|
+
const origin = originOf(sprite);
|
|
443
|
+
const m = sprite.transform ?? IDENTITY;
|
|
444
|
+
// in its box, where it is cut to a clip: placed from the clip's corner,
|
|
445
|
+
// and the box at the part's place among the layers
|
|
446
|
+
const clip = state.clip;
|
|
447
|
+
if (state.box) {
|
|
448
|
+
const frame = [clip.x / s, clip.y / s, clip.width / s, clip.height / s];
|
|
449
|
+
const was = state.boxProps;
|
|
450
|
+
if (!sameMatrix(was.frame, frame) || was.zPosition !== z) {
|
|
451
|
+
this.native.setLayerProps(state.box, {
|
|
452
|
+
frame,
|
|
453
|
+
masksToBounds: true,
|
|
454
|
+
zPosition: z,
|
|
455
|
+
});
|
|
456
|
+
state.boxProps = { frame, zPosition: z };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const ox = state.box ? clip.x : 0;
|
|
460
|
+
const oy = state.box ? clip.y : 0;
|
|
461
|
+
const next = {
|
|
462
|
+
bounds: [0, 0, rect.width / s, rect.height / s],
|
|
463
|
+
anchorPoint: [
|
|
464
|
+
(origin.x - rect.x) / rect.width,
|
|
465
|
+
(origin.y - rect.y) / rect.height,
|
|
466
|
+
],
|
|
467
|
+
position: [(origin.x - ox) / s, (origin.y - oy) / s],
|
|
468
|
+
zPosition: state.box ? 0 : z,
|
|
469
|
+
opacity: Math.min(1, Math.max(0, sprite.opacity ?? 1)),
|
|
470
|
+
matrix: m,
|
|
471
|
+
};
|
|
472
|
+
const prev = state.props;
|
|
473
|
+
const out = {};
|
|
474
|
+
let any = false;
|
|
475
|
+
for (const key of ['bounds', 'anchorPoint', 'position']) {
|
|
476
|
+
if (!sameMatrix(prev[key], next[key])) {
|
|
477
|
+
out[key] = next[key];
|
|
478
|
+
any = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
for (const key of ['zPosition', 'opacity']) {
|
|
482
|
+
if (prev[key] !== next[key]) {
|
|
483
|
+
out[key] = next[key];
|
|
484
|
+
any = true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!sameMatrix(prev.matrix, m)) {
|
|
488
|
+
out.transform = { matrix: [m[0], m[1], m[2], m[3], m[4] / s, m[5] / s] };
|
|
489
|
+
any = true;
|
|
490
|
+
}
|
|
491
|
+
if (any) this.native.setLayerProps(state.layer, out);
|
|
492
|
+
state.props = next;
|
|
493
|
+
// after the model went out, in the same transaction
|
|
494
|
+
this._syncAnimations(state, sprite);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
_paint(state, sprite, rect) {
|
|
498
|
+
const ctx = state.raster.ensure(
|
|
499
|
+
this.promotion,
|
|
500
|
+
rect.width,
|
|
501
|
+
rect.height,
|
|
502
|
+
this.scale,
|
|
503
|
+
);
|
|
504
|
+
const root = state.host.root;
|
|
505
|
+
ctx.save();
|
|
506
|
+
try {
|
|
507
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
508
|
+
ctx.translate(-rect.x, -rect.y);
|
|
509
|
+
// a part paints whole: there is no pass to cull it by
|
|
510
|
+
if (root) root._paintDamage = null;
|
|
511
|
+
sprite.paint(ctx);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
this._warn(
|
|
514
|
+
state.host,
|
|
515
|
+
`a sprite's paint() threw: ${err?.message ?? err}`,
|
|
516
|
+
);
|
|
517
|
+
} finally {
|
|
518
|
+
ctx.restore();
|
|
519
|
+
}
|
|
520
|
+
this.native.surfaceToLayer(state.raster.surface, state.layer);
|
|
521
|
+
state.painted = sprite.version;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** What runs on the layer, by the element's ids: one gone from the part
|
|
525
|
+
* comes off, one new to it goes on, and one already on is left alone —
|
|
526
|
+
* its `delay` is counted from the frame that asks, so it moves every
|
|
527
|
+
* frame and means nothing past the first. */
|
|
528
|
+
_syncAnimations(state, sprite) {
|
|
529
|
+
const wanted = sprite.animations ?? [];
|
|
530
|
+
const listed = (id) => wanted.some((a) => a.id === id);
|
|
531
|
+
for (const [id, run] of state.runs) {
|
|
532
|
+
if (listed(id)) continue;
|
|
533
|
+
this.native.removeAnimation(state.layer, run.caKey);
|
|
534
|
+
this._ends().delete(run.caId);
|
|
535
|
+
state.runs.delete(id);
|
|
536
|
+
}
|
|
537
|
+
for (const id of state.done) if (!listed(id)) state.done.delete(id);
|
|
538
|
+
const s = this.scale;
|
|
539
|
+
for (const a of wanted) {
|
|
540
|
+
if (state.runs.has(a.id) || state.done.has(a.id)) continue;
|
|
541
|
+
const caId = `rxs${++spriteSeq}`;
|
|
542
|
+
const caKey = `sprite:${caId}`;
|
|
543
|
+
const opts = {
|
|
544
|
+
values:
|
|
545
|
+
a.property === 'transform'
|
|
546
|
+
? a.values.map((m) => ({
|
|
547
|
+
matrix: [m[0], m[1], m[2], m[3], m[4] / s, m[5] / s],
|
|
548
|
+
}))
|
|
549
|
+
: a.values,
|
|
550
|
+
duration: a.duration / 1000,
|
|
551
|
+
id: caId,
|
|
552
|
+
};
|
|
553
|
+
if (a.keyTimes) opts.keyTimes = a.keyTimes;
|
|
554
|
+
if (a.timings) opts.timings = a.timings;
|
|
555
|
+
if (a.repeat !== undefined) opts.repeat = a.repeat;
|
|
556
|
+
if (a.autoreverse) opts.autoreverse = true;
|
|
557
|
+
if (a.hold) opts.hold = true;
|
|
558
|
+
// from the frame that asked: ahead, or already that far in
|
|
559
|
+
if (a.delay) opts.delay = a.delay / 1000;
|
|
560
|
+
try {
|
|
561
|
+
this.native.addAnimation(state.layer, a.property, opts, caKey);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
this._warn(
|
|
564
|
+
state.host,
|
|
565
|
+
`the bridge refused sprite animation ${JSON.stringify(a.id)}: ${err?.message ?? err}`,
|
|
566
|
+
);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
state.runs.set(a.id, { caId, caKey, held: Boolean(a.hold) });
|
|
570
|
+
this._ends().set(caId, (ev) => this._ended(state, a.id, caId, ev));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** The bridge's `animation-end` for one of ours. A held one stays on the
|
|
575
|
+
* layer, showing its last value, until the element stops listing it. */
|
|
576
|
+
_ended(state, id, caId, ev) {
|
|
577
|
+
this._ends().delete(caId);
|
|
578
|
+
const run = state.runs.get(id);
|
|
579
|
+
if (run?.caId === caId) {
|
|
580
|
+
state.done.add(id);
|
|
581
|
+
if (!run.held) state.runs.delete(id);
|
|
582
|
+
}
|
|
583
|
+
if (state.host.destroyed) return;
|
|
584
|
+
try {
|
|
585
|
+
state.host.spriteAnimationEnded(state.key, id, ev?.finished !== false);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
this._warn(
|
|
588
|
+
state.host,
|
|
589
|
+
`spriteAnimationEnded() threw: ${err?.message ?? err}`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Everything off the root and freed: the window is going. */
|
|
595
|
+
destroy() {
|
|
596
|
+
for (const lifted of this.hosts.values()) {
|
|
597
|
+
for (const state of lifted.values()) {
|
|
598
|
+
this.native.removeFromSuperlayer(state.box ?? state.layer);
|
|
599
|
+
state.raster.release(this.native);
|
|
600
|
+
for (const run of state.runs.values()) this._ends().delete(run.caId);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
this.hosts.clear();
|
|
604
|
+
}
|
|
605
|
+
}
|
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
|
}
|