react-x11 2.7.0 → 2.8.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 +52 -4
- package/src/cocoa/dnd.js +12 -1
- package/src/cocoa/presenter.js +243 -190
- package/src/cocoa/promotion.js +708 -0
- package/src/cocoa/window.js +29 -1
- package/src/dnd.js +70 -33
- package/src/index.d.ts +12 -1
- package/src/nodes.js +55 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"yoga-layout": "^3.2.1"
|
|
99
99
|
},
|
|
100
100
|
"optionalDependencies": {
|
|
101
|
-
"@windowkit/appkit": "^0.5.
|
|
101
|
+
"@windowkit/appkit": "^0.5.1",
|
|
102
102
|
"dbus-native": "^0.15.1",
|
|
103
103
|
"x11-dri": "^0.7.0"
|
|
104
104
|
},
|
package/src/cocoa/app.js
CHANGED
|
@@ -16,6 +16,7 @@ import { cssColorStraight } from 'ntk';
|
|
|
16
16
|
|
|
17
17
|
import { deliverActivate, deliverOpen } from '../application.js';
|
|
18
18
|
import { flushPendingFrames } from '../frames.js';
|
|
19
|
+
import { flushSyncWork } from '../priority.js';
|
|
19
20
|
import { setCompositingForTests } from '../compositing.js';
|
|
20
21
|
import { setScreensForTests } from '../screens.js';
|
|
21
22
|
import { setScaleForTests } from '../scale.js';
|
|
@@ -118,6 +119,23 @@ export class CocoaApp {
|
|
|
118
119
|
options.cocoa?.presenter ??
|
|
119
120
|
process.env.REACT_X11_COCOA_PRESENTER ??
|
|
120
121
|
'surface';
|
|
122
|
+
// With the surface presenter, whether the nodes that animate get a
|
|
123
|
+
// layer of their own above the bitmap (src/cocoa/promotion.js). On by
|
|
124
|
+
// default — an animation the render server runs survives a busy JS
|
|
125
|
+
// thread, and the bitmap keeps the frame for everything else — where
|
|
126
|
+
// the bridge draws a layer's colour and a rastered one the same, which
|
|
127
|
+
// it says with `colorSpace()` (@windowkit/appkit >= 0.5.1, the range
|
|
128
|
+
// package.json asks for): before it a layer's colour was Generic RGB
|
|
129
|
+
// against sRGB surfaces, and a node moving between its layer and the
|
|
130
|
+
// bitmap changed shade on the way.
|
|
131
|
+
// Off (`cocoa.promote: false`, REACT_X11_COCOA_PROMOTE=0) keeps every
|
|
132
|
+
// animation on the frame clock, which is what the presenter bench's
|
|
133
|
+
// `surface` column measures; on (`true`, `=1`) is on regardless.
|
|
134
|
+
const promoteEnv = process.env.REACT_X11_COCOA_PROMOTE;
|
|
135
|
+
this._promote =
|
|
136
|
+
options.cocoa?.promote ??
|
|
137
|
+
(promoteEnv === '1' ||
|
|
138
|
+
(promoteEnv !== '0' && native.colorSpace?.() === 'sRGB'));
|
|
121
139
|
|
|
122
140
|
// the app's own bridge, so an app over a fake one (the tests) needs no
|
|
123
141
|
// real bridge on the machine — the manager's default loads it only when
|
|
@@ -638,9 +656,21 @@ export class CocoaApp {
|
|
|
638
656
|
for (const wnd of this._windows.values()) wnd.present();
|
|
639
657
|
}
|
|
640
658
|
|
|
641
|
-
/**
|
|
642
|
-
*
|
|
659
|
+
/**
|
|
660
|
+
* After any synchronously dispatched input: land React's half of the
|
|
661
|
+
* response, paint it (the same early flush a discrete event gets on X11 —
|
|
662
|
+
* `discrete` in events.js) and put it on glass.
|
|
663
|
+
*
|
|
664
|
+
* `flushSyncWork` is not belt and braces. A discrete-priority update is
|
|
665
|
+
* committed on a **microtask**, and a microtask does not run until the
|
|
666
|
+
* call that dispatched the event returns to Node — which, inside one of
|
|
667
|
+
* AppKit's modal loops (a live resize, menu tracking, a dragging
|
|
668
|
+
* session), is after the whole gesture. Without it a handler's own
|
|
669
|
+
* `setState` reaches the screen when the gesture ends, and the gesture is
|
|
670
|
+
* exactly when it was worth showing.
|
|
671
|
+
*/
|
|
643
672
|
_afterInput() {
|
|
673
|
+
flushSyncWork();
|
|
644
674
|
flushPendingFrames();
|
|
645
675
|
this._presentAll();
|
|
646
676
|
}
|
|
@@ -694,10 +724,14 @@ export class CocoaApp {
|
|
|
694
724
|
case 'drag-perform':
|
|
695
725
|
return this._routeDrop(ev);
|
|
696
726
|
case 'drag-session-began':
|
|
727
|
+
// `draggingSession:willBeginAtPoint:`, dispatched from inside
|
|
728
|
+
// `beginDrag` — before the `onDragStart` handler's own render has
|
|
729
|
+
// been asked for, so there is nothing to flush yet. The frame that
|
|
730
|
+
// shows the drag beginning goes out from `CocoaWindow.beginDrag`,
|
|
731
|
+
// on the way back past it.
|
|
697
732
|
return undefined;
|
|
698
733
|
case 'drag-session-moved':
|
|
699
|
-
this.
|
|
700
|
-
return undefined;
|
|
734
|
+
return this._routeDragMoved(ev);
|
|
701
735
|
case 'drag-session-ended':
|
|
702
736
|
return this._routeDragEnded(ev);
|
|
703
737
|
case 'app-open-urls':
|
|
@@ -913,6 +947,20 @@ export class CocoaApp {
|
|
|
913
947
|
this._afterInput();
|
|
914
948
|
}
|
|
915
949
|
|
|
950
|
+
/**
|
|
951
|
+
* `draggingSession:movedToPoint:` — the source's own view of the gesture,
|
|
952
|
+
* and the only thing that arrives while AppKit tracks it: the pointer's
|
|
953
|
+
* `mousemove` stops, `pump2` does not return until the drop, and no timer
|
|
954
|
+
* or microtask of ours runs in between. So the frame belongs to the
|
|
955
|
+
* callback, like the destination's (`_routeDrop`) — that is what lets a
|
|
956
|
+
* `<popup dragPreview>` follow the pointer, and an `onDrag` that moves an
|
|
957
|
+
* insertion marker move it during the drag rather than on the release.
|
|
958
|
+
*/
|
|
959
|
+
_routeDragMoved(ev) {
|
|
960
|
+
this._activeDrag?.nativeMoved(ev);
|
|
961
|
+
this._afterInput();
|
|
962
|
+
}
|
|
963
|
+
|
|
916
964
|
/** The release of a drag AppKit was tracking for us. */
|
|
917
965
|
_routeDragEnded(ev) {
|
|
918
966
|
const drag = this._activeDrag;
|
package/src/cocoa/dnd.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
// identifier for a MIME type no declared type claims is computed alike by
|
|
45
45
|
// every process — how `application/x-myapp-…` travels between two react-x11
|
|
46
46
|
// apps) and back (`pasteboardTypeInfo`).
|
|
47
|
+
import { runWithPriority, DiscreteEventPriority } from '../priority.js';
|
|
47
48
|
import {
|
|
48
49
|
TEXT_TARGETS,
|
|
49
50
|
TYPE_GROUPS,
|
|
@@ -254,6 +255,15 @@ export class CocoaDropTransport {
|
|
|
254
255
|
this.session = session;
|
|
255
256
|
this.node = node;
|
|
256
257
|
this._registered = null;
|
|
258
|
+
// AppKit tracks the whole drag on this thread: the pump does not return
|
|
259
|
+
// until the drop, so nothing an enter/over dispatch schedules — a
|
|
260
|
+
// `useDropTarget` lighting its "drop here" label, any handler's
|
|
261
|
+
// `setState` — has a frame tick or a microtask to land on. Discrete is
|
|
262
|
+
// the one lane the app can land by hand from inside the callback
|
|
263
|
+
// (src/cocoa/app.js `_afterInput`), which is where the answer to
|
|
264
|
+
// AppKit's question is painted. The renderer's own `:drag-over` needs
|
|
265
|
+
// none of this and never did.
|
|
266
|
+
session.hoverPriority = DiscreteEventPriority;
|
|
257
267
|
this.refreshTypes();
|
|
258
268
|
}
|
|
259
269
|
|
|
@@ -326,7 +336,8 @@ export class CocoaDropTransport {
|
|
|
326
336
|
|
|
327
337
|
_leave(ev) {
|
|
328
338
|
const drag = this._local(ev);
|
|
329
|
-
|
|
339
|
+
// the leaving half of the same stream, in the same lane as the enter
|
|
340
|
+
runWithPriority(DiscreteEventPriority, () => this.session.localLeave());
|
|
330
341
|
if (drag) drag.accepted = false;
|
|
331
342
|
}
|
|
332
343
|
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { EASING_CONTROL_POINTS, TRANSITION_CONTROL_POINTS } from '../styles.js';
|
|
36
36
|
import { CocoaContext2D } from './context2d.js';
|
|
37
37
|
|
|
38
|
-
const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
38
|
+
export const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* A recording "context" for ntk's SvgView.draw: instead of rasterizing, it
|
|
@@ -325,7 +325,13 @@ const EDGE_PROPS = [
|
|
|
325
325
|
'borderEndWidth',
|
|
326
326
|
];
|
|
327
327
|
|
|
328
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Does `style` draw as a plain box — the property vocabulary of a layer
|
|
330
|
+
* (background, a uniform solid border, one radius), nothing that needs a
|
|
331
|
+
* raster? What decides whether a node is a PropBox on the layer presenter,
|
|
332
|
+
* and whether the surface presenter can promote it (src/cocoa/promotion.js).
|
|
333
|
+
*/
|
|
334
|
+
export function stylePaintsPlain(node, style = node.style ?? {}) {
|
|
329
335
|
if (node.kind !== 'box') return false;
|
|
330
336
|
if (style.backgroundImage || style.boxShadow || style.outlineWidth) {
|
|
331
337
|
return false;
|
|
@@ -359,13 +365,13 @@ const ANIMATED_KEY_PATHS = Object.freeze({
|
|
|
359
365
|
// looked up per app (`_animationEnds`)
|
|
360
366
|
let animationSeq = 0;
|
|
361
367
|
|
|
362
|
-
function uniformRadius(radius) {
|
|
368
|
+
export function uniformRadius(radius) {
|
|
363
369
|
if (radius === undefined) return 0;
|
|
364
370
|
if (typeof radius === 'number') return radius;
|
|
365
371
|
return null; // per-corner shapes go to raster
|
|
366
372
|
}
|
|
367
373
|
|
|
368
|
-
class Visual {
|
|
374
|
+
export class Visual {
|
|
369
375
|
constructor(presenter, node) {
|
|
370
376
|
this.presenter = presenter;
|
|
371
377
|
this.node = node;
|
|
@@ -409,7 +415,7 @@ class Visual {
|
|
|
409
415
|
}
|
|
410
416
|
}
|
|
411
417
|
|
|
412
|
-
class RasterState {
|
|
418
|
+
export class RasterState {
|
|
413
419
|
constructor() {
|
|
414
420
|
this.surface = null;
|
|
415
421
|
this.gen = 0;
|
|
@@ -450,6 +456,218 @@ class RasterState {
|
|
|
450
456
|
}
|
|
451
457
|
}
|
|
452
458
|
|
|
459
|
+
/**
|
|
460
|
+
* A plain box as the properties of its layer — what a PropBox visual sends
|
|
461
|
+
* on the layer presenter and what a promoted node's layer is set from on
|
|
462
|
+
* the surface presenter (src/cocoa/promotion.js): one function, so a box
|
|
463
|
+
* reads the same on either. `parentOrigin` is what `frame` is relative to,
|
|
464
|
+
* in window coordinates; everything goes out in points.
|
|
465
|
+
*/
|
|
466
|
+
export function propBoxProps(node, app, scale, parentOrigin, order) {
|
|
467
|
+
const abs = node.abs;
|
|
468
|
+
const style = node.style ?? {};
|
|
469
|
+
const border = typeof style.borderWidth === 'number' ? style.borderWidth : 0;
|
|
470
|
+
const colour = (value) =>
|
|
471
|
+
value ? (app._parseColor(String(value)) ?? [0, 0, 0, 0]) : [0, 0, 0, 0];
|
|
472
|
+
return {
|
|
473
|
+
frame: [
|
|
474
|
+
(abs.x - parentOrigin.x) / scale,
|
|
475
|
+
(abs.y - parentOrigin.y) / scale,
|
|
476
|
+
Math.max(0, abs.width) / scale,
|
|
477
|
+
Math.max(0, abs.height) / scale,
|
|
478
|
+
],
|
|
479
|
+
zPosition: order,
|
|
480
|
+
hidden: Boolean(node.hidden),
|
|
481
|
+
masksToBounds: Boolean(node.clipsChildren?.()) && !node.isScroller?.(),
|
|
482
|
+
cornerRadius: (uniformRadius(style.borderRadius) ?? 0) / scale,
|
|
483
|
+
backgroundColor: colour(style.backgroundColor),
|
|
484
|
+
borderWidth: border / scale,
|
|
485
|
+
borderColor: colour(style.borderColor),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// --- animations the render server runs ---------------------------------------
|
|
490
|
+
//
|
|
491
|
+
// The node model keeps deciding what is animating and when it ends
|
|
492
|
+
// (nodes.js `_retarget` / `_updateLoops`); what moves here is who
|
|
493
|
+
// interpolates. Taken means the node's style goes to its target — the
|
|
494
|
+
// layer's model value, sent by the next frame's property diff — and that
|
|
495
|
+
// frame attaches an explicit animation carrying the pixels there; no frame
|
|
496
|
+
// after it is scheduled for the property, and a loop costs no JS frames at
|
|
497
|
+
// all. Declined means the frame clock runs it exactly as before, so
|
|
498
|
+
// nothing here is load-bearing for correctness.
|
|
499
|
+
//
|
|
500
|
+
// Kept apart from the presenter because two of them hand animations over:
|
|
501
|
+
// the layer presenter, where every plain box has a layer, and the surface
|
|
502
|
+
// presenter's promotion (src/cocoa/promotion.js), where only the nodes
|
|
503
|
+
// that animate do. They differ in which node has a layer and agree on
|
|
504
|
+
// everything from there — `layerOf(node)` is the whole of the difference.
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The animations one presenter has handed to the render server: what
|
|
508
|
+
* `take` accepted and is waiting for the frame that attaches it, and what
|
|
509
|
+
* is running on a layer. `onIdle(node)`, when given, hears that the last
|
|
510
|
+
* animation running for a node ended on the bridge's word — the one end
|
|
511
|
+
* no frame follows on its own.
|
|
512
|
+
*/
|
|
513
|
+
export class LayerAnimations {
|
|
514
|
+
constructor({ native, app, scale, layerOf, onIdle = null }) {
|
|
515
|
+
this.native = native;
|
|
516
|
+
this.app = app;
|
|
517
|
+
this.scale = scale;
|
|
518
|
+
this.layerOf = layerOf;
|
|
519
|
+
this.onIdle = onIdle;
|
|
520
|
+
this.pending = new Map(); // node -> Map(prop -> entry)
|
|
521
|
+
this.live = new Map(); // node -> Map(id -> { prop, key, entry })
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Take `prop`'s animation for `node`, or decline. Decided against the
|
|
526
|
+
* *target* style: a `:hover` that adds a shadow turns the node into a
|
|
527
|
+
* raster in the same swap that starts a fade, and a raster's background
|
|
528
|
+
* is in its bitmap, not on its layer.
|
|
529
|
+
*/
|
|
530
|
+
take(node, prop, entry) {
|
|
531
|
+
const map = ANIMATED_KEY_PATHS[prop];
|
|
532
|
+
if (!map || !stylePaintsPlain(node, node._targetStyle ?? node.style)) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
if (this._value(map, entry.from) == null) return false;
|
|
536
|
+
if (this._value(map, entry.to) == null) return false;
|
|
537
|
+
let pending = this.pending.get(node);
|
|
538
|
+
if (!pending) this.pending.set(node, (pending = new Map()));
|
|
539
|
+
pending.set(prop, entry);
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Stop what runs for `prop` on `node`: a loop the window lost sight of,
|
|
544
|
+
* a declaration that changed, a transition the clock takes back. */
|
|
545
|
+
cancel(node, prop) {
|
|
546
|
+
this.pending.get(node)?.delete(prop);
|
|
547
|
+
this._removeLive(node, prop);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Is anything taken for `node` — waiting for a frame, or running? */
|
|
551
|
+
has(node) {
|
|
552
|
+
return this.pending.has(node) || this.live.has(node);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
_ends() {
|
|
556
|
+
return (this.app._animationEnds ??= new Map());
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** A style value as the layer takes it — a colour as components, a
|
|
560
|
+
* length in points — or null for one the layer cannot animate. */
|
|
561
|
+
_value(map, value) {
|
|
562
|
+
if (map.colour) {
|
|
563
|
+
return typeof value === 'string'
|
|
564
|
+
? (this.app._parseColor(value) ?? null)
|
|
565
|
+
: null;
|
|
566
|
+
}
|
|
567
|
+
return typeof value === 'number'
|
|
568
|
+
? value / (map.scaled ? this.scale : 1)
|
|
569
|
+
: null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** The frame's half: attach what `take` accepted to `layer`, after the
|
|
573
|
+
* model value went out, inside the same transaction. */
|
|
574
|
+
apply(node, layer) {
|
|
575
|
+
const pending = this.pending.get(node);
|
|
576
|
+
if (!pending) return;
|
|
577
|
+
this.pending.delete(node);
|
|
578
|
+
for (const [prop, entry] of pending) {
|
|
579
|
+
const map = ANIMATED_KEY_PATHS[prop];
|
|
580
|
+
const id = `rx${++animationSeq}`;
|
|
581
|
+
const key = `${prop}:${id}`;
|
|
582
|
+
const opts = { duration: entry.duration / 1000, id };
|
|
583
|
+
if (entry.loop) {
|
|
584
|
+
// a loop replaces whatever ran for the property: its declaration
|
|
585
|
+
// changed, and a loop restarts from the top when it does
|
|
586
|
+
this._removeLive(node, prop);
|
|
587
|
+
opts.from = this._value(map, entry.from);
|
|
588
|
+
opts.to = this._value(map, entry.to);
|
|
589
|
+
opts.timing = EASING_CONTROL_POINTS[entry.easing];
|
|
590
|
+
opts.repeat = Infinity;
|
|
591
|
+
opts.autoreverse = entry.alternate;
|
|
592
|
+
} else if (map.colour) {
|
|
593
|
+
// From where the pixels are — which is what "an interrupted
|
|
594
|
+
// transition reverses from where it got to" means here. A colour
|
|
595
|
+
// cannot be additive, so the one before it is replaced.
|
|
596
|
+
this._removeLive(node, prop);
|
|
597
|
+
const shown = this.native.presentationValue?.(layer, map.keyPath);
|
|
598
|
+
opts.from = Array.isArray(shown) ? shown : this._value(map, entry.from);
|
|
599
|
+
opts.to = this._value(map, entry.to);
|
|
600
|
+
opts.timing = TRANSITION_CONTROL_POINTS;
|
|
601
|
+
} else {
|
|
602
|
+
// Additive: a delta over the model value, (old − new) → 0, and the
|
|
603
|
+
// ones before it keep running and sum. Continuity on a retarget with
|
|
604
|
+
// nothing read back, however many are in flight.
|
|
605
|
+
opts.from = this._value(map, entry.from) - this._value(map, entry.to);
|
|
606
|
+
opts.to = 0;
|
|
607
|
+
opts.additive = true;
|
|
608
|
+
opts.timing = TRANSITION_CONTROL_POINTS;
|
|
609
|
+
}
|
|
610
|
+
this.native.addAnimation(layer, map.keyPath, opts, key);
|
|
611
|
+
// looked up after the removals above, which may have pruned the map
|
|
612
|
+
let live = this.live.get(node);
|
|
613
|
+
if (!live) this.live.set(node, (live = new Map()));
|
|
614
|
+
live.set(id, { prop, key, entry });
|
|
615
|
+
this._ends().set(id, (ev) => this._animationEnded(node, id, ev));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Every animation running for `prop` on `node`, off the layer and out
|
|
620
|
+
* of the books. */
|
|
621
|
+
_removeLive(node, prop) {
|
|
622
|
+
const live = this.live.get(node);
|
|
623
|
+
if (!live) return;
|
|
624
|
+
const layer = this.layerOf(node);
|
|
625
|
+
for (const [id, run] of live) {
|
|
626
|
+
if (run.prop !== prop) continue;
|
|
627
|
+
live.delete(id);
|
|
628
|
+
this._ends().delete(id);
|
|
629
|
+
if (layer) this.native.removeAnimation(layer, run.key);
|
|
630
|
+
}
|
|
631
|
+
if (live.size === 0) this.live.delete(node);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** The bridge's `animation-end` for one of ours — it ran out, or CA
|
|
635
|
+
* dropped it. An older additive one ending changes nothing: the node
|
|
636
|
+
* checks the entry is still the one it holds. */
|
|
637
|
+
_animationEnded(node, id) {
|
|
638
|
+
this._ends().delete(id);
|
|
639
|
+
const live = this.live.get(node);
|
|
640
|
+
const run = live?.get(id);
|
|
641
|
+
if (!run) return;
|
|
642
|
+
live.delete(id);
|
|
643
|
+
if (live.size === 0) this.live.delete(node);
|
|
644
|
+
node._offloadEnded(run.prop, run.entry);
|
|
645
|
+
if (!this.has(node)) this.onIdle?.(node);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** The layer is going — the visual is destroyed, or turns into a raster —
|
|
649
|
+
* and every animation on it goes with it. What was still waiting for a
|
|
650
|
+
* frame goes back to the frame clock when the node stays (`reclaim`);
|
|
651
|
+
* what was running is over, and the model shows. */
|
|
652
|
+
drop(node, reclaim) {
|
|
653
|
+
const pending = this.pending.get(node);
|
|
654
|
+
if (pending) {
|
|
655
|
+
this.pending.delete(node);
|
|
656
|
+
for (const [prop, entry] of pending) {
|
|
657
|
+
if (reclaim) node._offloadDeclined(prop, entry);
|
|
658
|
+
else node._offloadEnded(prop, entry);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const live = this.live.get(node);
|
|
662
|
+
if (!live) return;
|
|
663
|
+
this.live.delete(node);
|
|
664
|
+
for (const [id, run] of live) {
|
|
665
|
+
this._ends().delete(id);
|
|
666
|
+
node._offloadEnded(run.prop, run.entry);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
453
671
|
export class CocoaLayerPresenter {
|
|
454
672
|
constructor(window) {
|
|
455
673
|
this.window = window;
|
|
@@ -459,10 +677,13 @@ export class CocoaLayerPresenter {
|
|
|
459
677
|
this.visuals = new Map(); // node -> Visual
|
|
460
678
|
this.rasters = new Map(); // node -> RasterState
|
|
461
679
|
this.bars = new Map(); // scroller node -> Map(axis -> { layer, raster })
|
|
462
|
-
// animations taken off the frame clock
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
680
|
+
// animations taken off the frame clock, by the node's own layer
|
|
681
|
+
this.animations = new LayerAnimations({
|
|
682
|
+
native: this.native,
|
|
683
|
+
app: window.app,
|
|
684
|
+
scale: this.scale,
|
|
685
|
+
layerOf: (node) => this.visuals.get(node)?.layer ?? null,
|
|
686
|
+
});
|
|
466
687
|
// Claims since the last frame — taken at the top of `frame()`, the way
|
|
467
688
|
// the X11 path takes its damage before painting, so a claim made from
|
|
468
689
|
// inside a paint lands in the next frame instead of being cleared with
|
|
@@ -572,7 +793,7 @@ export class CocoaLayerPresenter {
|
|
|
572
793
|
visual.destroy();
|
|
573
794
|
this.visuals.delete(node);
|
|
574
795
|
this._dropRaster(node);
|
|
575
|
-
this.
|
|
796
|
+
this.animations.drop(node, false);
|
|
576
797
|
const bars = this.bars.get(node);
|
|
577
798
|
if (bars) {
|
|
578
799
|
for (const entry of bars.values()) {
|
|
@@ -639,11 +860,11 @@ export class CocoaLayerPresenter {
|
|
|
639
860
|
this._dropRaster(node);
|
|
640
861
|
// a layer that turns into a raster takes its animations with it; the
|
|
641
862
|
// frame clock can still run them over the bitmap
|
|
642
|
-
if (wantsRaster) this.
|
|
863
|
+
if (wantsRaster) this.animations.drop(node, true);
|
|
643
864
|
visual = null;
|
|
644
865
|
}
|
|
645
|
-
if (wantsRaster && this.
|
|
646
|
-
this.
|
|
866
|
+
if (wantsRaster && this.animations.pending.has(node)) {
|
|
867
|
+
this.animations.drop(node, true);
|
|
647
868
|
}
|
|
648
869
|
if (!visual) {
|
|
649
870
|
visual = new Visual(this, node);
|
|
@@ -731,191 +952,23 @@ export class CocoaLayerPresenter {
|
|
|
731
952
|
|
|
732
953
|
_syncPropBox(node, visual, parentOrigin, order) {
|
|
733
954
|
const abs = node.abs;
|
|
734
|
-
const style = node.style ?? {};
|
|
735
|
-
const s = this.scale;
|
|
736
955
|
visual.origin = { x: abs.x, y: abs.y };
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
frame: [
|
|
741
|
-
(abs.x - parentOrigin.x) / s,
|
|
742
|
-
(abs.y - parentOrigin.y) / s,
|
|
743
|
-
Math.max(0, abs.width) / s,
|
|
744
|
-
Math.max(0, abs.height) / s,
|
|
745
|
-
],
|
|
746
|
-
zPosition: order,
|
|
747
|
-
hidden: Boolean(node.hidden),
|
|
748
|
-
masksToBounds: Boolean(node.clipsChildren?.()) && !node.isScroller?.(),
|
|
749
|
-
cornerRadius: (uniformRadius(style.borderRadius) ?? 0) / s,
|
|
750
|
-
backgroundColor: style.backgroundColor
|
|
751
|
-
? (this.window.app._parseColor(String(style.backgroundColor)) ?? [
|
|
752
|
-
0, 0, 0, 0,
|
|
753
|
-
])
|
|
754
|
-
: [0, 0, 0, 0],
|
|
755
|
-
borderWidth: border / s,
|
|
756
|
-
borderColor: style.borderColor
|
|
757
|
-
? (this.window.app._parseColor(String(style.borderColor)) ?? [
|
|
758
|
-
0, 0, 0, 0,
|
|
759
|
-
])
|
|
760
|
-
: [0, 0, 0, 0],
|
|
761
|
-
});
|
|
956
|
+
visual.set(
|
|
957
|
+
propBoxProps(node, this.window.app, this.scale, parentOrigin, order),
|
|
958
|
+
);
|
|
762
959
|
// after the model value went out, inside the same transaction
|
|
763
|
-
this.
|
|
960
|
+
this.animations.apply(node, visual.layer);
|
|
764
961
|
}
|
|
765
962
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
// The node model keeps deciding what is animating and when it ends
|
|
769
|
-
// (nodes.js `_retarget` / `_updateLoops`); what moves here is who
|
|
770
|
-
// interpolates. Taken means the node's style goes to its target — the
|
|
771
|
-
// layer's model value, sent by the next frame's property diff — and that
|
|
772
|
-
// frame attaches an explicit animation carrying the pixels there; no frame
|
|
773
|
-
// after it is scheduled for the property, and a loop costs no JS frames at
|
|
774
|
-
// all. Declined means the frame clock runs it exactly as before, so
|
|
775
|
-
// nothing here is load-bearing for correctness.
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Take `prop`'s animation for `node`, or decline. Decided against the
|
|
779
|
-
* *target* style: a `:hover` that adds a shadow turns the node into a
|
|
780
|
-
* raster in the same swap that starts a fade, and a raster's background
|
|
781
|
-
* is in its bitmap, not on its layer.
|
|
782
|
-
*/
|
|
963
|
+
/** The window's animation seam (src/cocoa/window.js): take `prop`'s
|
|
964
|
+
* animation for `node` off the frame clock, or decline. */
|
|
783
965
|
animate(node, prop, entry) {
|
|
784
|
-
|
|
785
|
-
if (!map || !stylePaintsPlain(node, node._targetStyle ?? node.style)) {
|
|
786
|
-
return false;
|
|
787
|
-
}
|
|
788
|
-
if (this._value(map, entry.from) == null) return false;
|
|
789
|
-
if (this._value(map, entry.to) == null) return false;
|
|
790
|
-
let pending = this.pendingAnimations.get(node);
|
|
791
|
-
if (!pending) this.pendingAnimations.set(node, (pending = new Map()));
|
|
792
|
-
pending.set(prop, entry);
|
|
793
|
-
return true;
|
|
966
|
+
return this.animations.take(node, prop, entry);
|
|
794
967
|
}
|
|
795
968
|
|
|
796
|
-
/**
|
|
797
|
-
* a declaration that changed, a transition the clock takes back. */
|
|
969
|
+
/** …and stop what runs for `prop` on `node`. */
|
|
798
970
|
cancel(node, prop) {
|
|
799
|
-
this.
|
|
800
|
-
this._removeLive(node, prop);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
_ends() {
|
|
804
|
-
const app = this.window.app;
|
|
805
|
-
return (app._animationEnds ??= new Map());
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/** A style value as the layer takes it — a colour as components, a
|
|
809
|
-
* length in points — or null for one the layer cannot animate. */
|
|
810
|
-
_value(map, value) {
|
|
811
|
-
if (map.colour) {
|
|
812
|
-
return typeof value === 'string'
|
|
813
|
-
? (this.window.app._parseColor(value) ?? null)
|
|
814
|
-
: null;
|
|
815
|
-
}
|
|
816
|
-
return typeof value === 'number'
|
|
817
|
-
? value / (map.scaled ? this.scale : 1)
|
|
818
|
-
: null;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/** The frame's half: attach what `animate` accepted, after the model
|
|
822
|
-
* value went out, inside the same transaction. */
|
|
823
|
-
_applyAnimations(node, visual) {
|
|
824
|
-
const pending = this.pendingAnimations.get(node);
|
|
825
|
-
if (!pending) return;
|
|
826
|
-
this.pendingAnimations.delete(node);
|
|
827
|
-
for (const [prop, entry] of pending) {
|
|
828
|
-
const map = ANIMATED_KEY_PATHS[prop];
|
|
829
|
-
const id = `rx${++animationSeq}`;
|
|
830
|
-
const key = `${prop}:${id}`;
|
|
831
|
-
const opts = { duration: entry.duration / 1000, id };
|
|
832
|
-
if (entry.loop) {
|
|
833
|
-
// a loop replaces whatever ran for the property: its declaration
|
|
834
|
-
// changed, and a loop restarts from the top when it does
|
|
835
|
-
this._removeLive(node, prop);
|
|
836
|
-
opts.from = this._value(map, entry.from);
|
|
837
|
-
opts.to = this._value(map, entry.to);
|
|
838
|
-
opts.timing = EASING_CONTROL_POINTS[entry.easing];
|
|
839
|
-
opts.repeat = Infinity;
|
|
840
|
-
opts.autoreverse = entry.alternate;
|
|
841
|
-
} else if (map.colour) {
|
|
842
|
-
// From where the pixels are — which is what "an interrupted
|
|
843
|
-
// transition reverses from where it got to" means here. A colour
|
|
844
|
-
// cannot be additive, so the one before it is replaced.
|
|
845
|
-
this._removeLive(node, prop);
|
|
846
|
-
const shown = this.native.presentationValue?.(
|
|
847
|
-
visual.layer,
|
|
848
|
-
map.keyPath,
|
|
849
|
-
);
|
|
850
|
-
opts.from = Array.isArray(shown) ? shown : this._value(map, entry.from);
|
|
851
|
-
opts.to = this._value(map, entry.to);
|
|
852
|
-
opts.timing = TRANSITION_CONTROL_POINTS;
|
|
853
|
-
} else {
|
|
854
|
-
// Additive: a delta over the model value, (old − new) → 0, and the
|
|
855
|
-
// ones before it keep running and sum. Continuity on a retarget with
|
|
856
|
-
// nothing read back, however many are in flight.
|
|
857
|
-
opts.from = this._value(map, entry.from) - this._value(map, entry.to);
|
|
858
|
-
opts.to = 0;
|
|
859
|
-
opts.additive = true;
|
|
860
|
-
opts.timing = TRANSITION_CONTROL_POINTS;
|
|
861
|
-
}
|
|
862
|
-
this.native.addAnimation(visual.layer, map.keyPath, opts, key);
|
|
863
|
-
// looked up after the removals above, which may have pruned the map
|
|
864
|
-
let live = this.liveAnimations.get(node);
|
|
865
|
-
if (!live) this.liveAnimations.set(node, (live = new Map()));
|
|
866
|
-
live.set(id, { prop, key, entry });
|
|
867
|
-
this._ends().set(id, (ev) => this._animationEnded(node, id, ev));
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/** Every animation running for `prop` on `node`, off the layer and out
|
|
872
|
-
* of the books. */
|
|
873
|
-
_removeLive(node, prop) {
|
|
874
|
-
const live = this.liveAnimations.get(node);
|
|
875
|
-
if (!live) return;
|
|
876
|
-
const visual = this.visuals.get(node);
|
|
877
|
-
for (const [id, run] of live) {
|
|
878
|
-
if (run.prop !== prop) continue;
|
|
879
|
-
live.delete(id);
|
|
880
|
-
this._ends().delete(id);
|
|
881
|
-
if (visual) this.native.removeAnimation(visual.layer, run.key);
|
|
882
|
-
}
|
|
883
|
-
if (live.size === 0) this.liveAnimations.delete(node);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/** The bridge's `animation-end` for one of ours — it ran out, or CA
|
|
887
|
-
* dropped it. An older additive one ending changes nothing: the node
|
|
888
|
-
* checks the entry is still the one it holds. */
|
|
889
|
-
_animationEnded(node, id) {
|
|
890
|
-
this._ends().delete(id);
|
|
891
|
-
const live = this.liveAnimations.get(node);
|
|
892
|
-
const run = live?.get(id);
|
|
893
|
-
if (!run) return;
|
|
894
|
-
live.delete(id);
|
|
895
|
-
if (live.size === 0) this.liveAnimations.delete(node);
|
|
896
|
-
node._offloadEnded(run.prop, run.entry);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/** The layer is going — the visual is destroyed, or turns into a raster —
|
|
900
|
-
* and every animation on it goes with it. What was still waiting for a
|
|
901
|
-
* frame goes back to the frame clock when the node stays (`reclaim`);
|
|
902
|
-
* what was running is over, and the model shows. */
|
|
903
|
-
_dropAnimations(node, reclaim) {
|
|
904
|
-
const pending = this.pendingAnimations.get(node);
|
|
905
|
-
if (pending) {
|
|
906
|
-
this.pendingAnimations.delete(node);
|
|
907
|
-
for (const [prop, entry] of pending) {
|
|
908
|
-
if (reclaim) node._offloadDeclined(prop, entry);
|
|
909
|
-
else node._offloadEnded(prop, entry);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
const live = this.liveAnimations.get(node);
|
|
913
|
-
if (!live) return;
|
|
914
|
-
this.liveAnimations.delete(node);
|
|
915
|
-
for (const [id, run] of live) {
|
|
916
|
-
this._ends().delete(id);
|
|
917
|
-
node._offloadEnded(run.prop, run.entry);
|
|
918
|
-
}
|
|
971
|
+
this.animations.cancel(node, prop);
|
|
919
972
|
}
|
|
920
973
|
|
|
921
974
|
/**
|