react-x11 2.35.0 → 2.37.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 +1 -1
- package/src/appearance.js +48 -15
- package/src/appearancehooks.js +1 -1
- package/src/backend/context2d.js +94 -3
- package/src/cocoa/presenter.js +148 -13
- package/src/cocoa/promotion.js +370 -33
- package/src/cocoa/sprites.js +40 -1
- package/src/cocoa/window.js +4 -0
- package/src/components/ProgressBar.js +7 -3
- package/src/nodes/animation.js +171 -12
- package/src/nodes/scrollable.js +3 -0
- package/src/nodes/window/flush.js +9 -0
- package/src/testing/harness.js +148 -9
- package/src/testing/index.d.ts +46 -1
- package/src/testing/index.js +1 -0
- package/src/types/appearance.d.ts +2 -2
- package/src/types/system.d.ts +6 -0
package/src/cocoa/promotion.js
CHANGED
|
@@ -23,16 +23,19 @@
|
|
|
23
23
|
// part: a promoted layer sits above ALL the 2D content, so a node is
|
|
24
24
|
// promoted only when nothing painted after it in the walk reaches into its
|
|
25
25
|
// bounds — no later sibling at any level, no ancestor's border ring or
|
|
26
|
-
// focus ring
|
|
27
|
-
// the whole of it, because the layer
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
26
|
+
// focus ring — and only when every clipping ancestor with round corners
|
|
27
|
+
// holds the whole of it, because the layer is not cut to a curve; one with
|
|
28
|
+
// square corners cuts the layer to its box, a row half out of its list as
|
|
29
|
+
// much as a spinner inside a card. A scrollbar's thumb drawn over it goes
|
|
30
|
+
// on a layer of its own above it instead (`_syncThumbs`). And no ancestor
|
|
31
|
+
// may fade, because the layer would not be faded with it. Declining is always
|
|
32
|
+
// safe: the frame clock runs the animation exactly as it does without this
|
|
33
|
+
// file. The same test runs again every frame, so a node that becomes
|
|
34
|
+
// overlapped, hidden, clipped, faded or non-plain returns to the bitmap in
|
|
35
|
+
// the frame that finds it, the animation handed back to the clock — and a
|
|
36
|
+
// loop the clock runs is offered again once the scene would take it
|
|
37
|
+
// (`wouldTake`), where it is in its cycle. docs/macos.md §"Layer
|
|
38
|
+
// promotion" is the account.
|
|
36
39
|
import { BoxNode } from '../nodes/box.js';
|
|
37
40
|
import { addDamageRect, damageToPaint } from '../nodes/damage.js';
|
|
38
41
|
import { intersectRects } from '../nodes/rects.js';
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
RASTER_PAD,
|
|
43
46
|
RasterState,
|
|
44
47
|
Visual,
|
|
48
|
+
movesNode,
|
|
45
49
|
propBoxProps,
|
|
46
50
|
stylePaintsPlain,
|
|
47
51
|
} from './presenter.js';
|
|
@@ -49,6 +53,15 @@ import { SpriteLayers } from './sprites.js';
|
|
|
49
53
|
|
|
50
54
|
const ORIGIN = Object.freeze({ x: 0, y: 0 });
|
|
51
55
|
|
|
56
|
+
// A pane's thumbs come after everything inside it in the paint order — the
|
|
57
|
+
// bars are painted over the pane's children — and before whatever is
|
|
58
|
+
// painted after the pane: its key with this past any child's index.
|
|
59
|
+
const AFTER_CHILDREN = 1e9;
|
|
60
|
+
|
|
61
|
+
// What core paints a thumb in when the pane names no colour
|
|
62
|
+
// (`paintScrollbarThumb`).
|
|
63
|
+
const THUMB_COLOUR = 'rgba(0, 0, 0, 0.25)';
|
|
64
|
+
|
|
52
65
|
// How long a node that has stopped animating keeps its layer. A hover card
|
|
53
66
|
// fades in and, a moment later, out; a palette step ends one transition and
|
|
54
67
|
// starts the next; a toast pulses again. Demoting on the last frame of each
|
|
@@ -207,9 +220,23 @@ function promotableNode(node) {
|
|
|
207
220
|
return !paintsOutline(node);
|
|
208
221
|
}
|
|
209
222
|
|
|
210
|
-
/**
|
|
211
|
-
* antialiasing needs.
|
|
212
|
-
|
|
223
|
+
/** Where a scrollbar puts ink: its thumb, with the pad the thumb's
|
|
224
|
+
* antialiasing needs. The track is not drawn, so a layer beside the thumb
|
|
225
|
+
* covers nothing of the bar's; one the thumb is drawn over has the thumb
|
|
226
|
+
* lifted above it (`_thumbsLiftable`). */
|
|
227
|
+
function scrollbarInk(bar, scale) {
|
|
228
|
+
const pad = Math.ceil(2 * scale);
|
|
229
|
+
return {
|
|
230
|
+
x: bar.x - pad,
|
|
231
|
+
y: bar.y - pad,
|
|
232
|
+
width: bar.width + 2 * pad,
|
|
233
|
+
height: bar.height + 2 * pad,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** …and everywhere the thumb can go, which a lifted thumb's layer may come
|
|
238
|
+
* to stand over as the pane scrolls. */
|
|
239
|
+
function scrollbarTrack(bar, scale) {
|
|
213
240
|
const pad = Math.ceil(2 * scale);
|
|
214
241
|
return bar.axis === 'x'
|
|
215
242
|
? {
|
|
@@ -226,6 +253,38 @@ function scrollbarStrip(bar, scale) {
|
|
|
226
253
|
};
|
|
227
254
|
}
|
|
228
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The clip a node's layer is cut to: the one `_clear` narrowed its reach to
|
|
258
|
+
* through the clipping ancestors around it, where that leaves any of the
|
|
259
|
+
* node out — and null where every one of them holds it whole, which keeps a
|
|
260
|
+
* node clear of its pane's edges on the window root as before. A rounded
|
|
261
|
+
* clip comes with its radius, and with the square clips that cut it again
|
|
262
|
+
* (`outer`), as an element's part's does.
|
|
263
|
+
*/
|
|
264
|
+
function clipOf(cut, reach) {
|
|
265
|
+
if (!cut.clip) return null;
|
|
266
|
+
const radius = cut.radius > 0 ? cut.radius : 0;
|
|
267
|
+
const held = radius > 0 ? insetRect(cut.clip, radius) : cut.clip;
|
|
268
|
+
if (!cut.outer && containsRect(held, reach)) return null;
|
|
269
|
+
return { rect: cut.clip, radius, outer: cut.outer ?? null };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Where a node's layer can be. Where the layout put it and what it holds
|
|
274
|
+
* reach — or, for a node a loop moves (an inset on a node out of the flow,
|
|
275
|
+
* which the render server runs as the layer's position, presenter.js
|
|
276
|
+
* `INSET_KEY_PATHS`), anywhere in the nearest box that clips it, the reach
|
|
277
|
+
* every frame of the loop stays inside (nodes/animation.js `_loopReach`).
|
|
278
|
+
* Null where nothing short of the window holds it, which no layer is
|
|
279
|
+
* cleared for.
|
|
280
|
+
*/
|
|
281
|
+
function reachOf(node) {
|
|
282
|
+
if (!node._loops?.some((loop) => movesNode(loop.prop))) {
|
|
283
|
+
return node._subtreeBounds();
|
|
284
|
+
}
|
|
285
|
+
return node._loopReach?.()?.rect ?? null;
|
|
286
|
+
}
|
|
287
|
+
|
|
229
288
|
/**
|
|
230
289
|
* The surface window's promoted nodes: which ones have a layer, what each
|
|
231
290
|
* layer shows, and the animations the render server runs on them.
|
|
@@ -263,6 +322,11 @@ export class CocoaPromotion {
|
|
|
263
322
|
// the parts of elements' drawing they offer (`Node.sprites()`), each on
|
|
264
323
|
// a layer of its own under the same rules (src/cocoa/sprites.js)
|
|
265
324
|
this.sprites = new SpriteLayers(this);
|
|
325
|
+
// the scroll panes whose thumbs are on layers of their own, above the
|
|
326
|
+
// layers they are drawn over (`_syncThumbs`), and the ones this frame's
|
|
327
|
+
// layers are under
|
|
328
|
+
this.thumbs = new Map(); // pane -> Map(axis -> { layer, props })
|
|
329
|
+
this._thumbsWanted = null;
|
|
266
330
|
this._claiming = false;
|
|
267
331
|
}
|
|
268
332
|
|
|
@@ -417,12 +481,14 @@ export class CocoaPromotion {
|
|
|
417
481
|
this.promoted.size === 0 &&
|
|
418
482
|
this.candidates.size === 0 &&
|
|
419
483
|
offers.size === 0 &&
|
|
420
|
-
this.sprites.hosts.size === 0
|
|
484
|
+
this.sprites.hosts.size === 0 &&
|
|
485
|
+
this.thumbs.size === 0
|
|
421
486
|
) {
|
|
422
487
|
return;
|
|
423
488
|
}
|
|
424
489
|
const native = this.native;
|
|
425
490
|
let lifted = null;
|
|
491
|
+
const wanted = (this._thumbsWanted = new Set());
|
|
426
492
|
native.txBegin({ disableActions: true });
|
|
427
493
|
try {
|
|
428
494
|
// Later-painted first: a node painted after this one that is coming
|
|
@@ -439,8 +505,11 @@ export class CocoaPromotion {
|
|
|
439
505
|
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
440
506
|
const { node, key } = candidates[i];
|
|
441
507
|
if (this.promoted.has(node) || !this.animations.has(node)) continue;
|
|
442
|
-
|
|
443
|
-
|
|
508
|
+
const cut = { thumbs: new Set(), round: true };
|
|
509
|
+
const reach = key && promotableNode(node) ? reachOf(node) : null;
|
|
510
|
+
if (reach && this._clear(node, reach, cut)) {
|
|
511
|
+
for (const pane of cut.thumbs) wanted.add(pane);
|
|
512
|
+
this._promote(node, root, clipOf(cut, reach));
|
|
444
513
|
} else {
|
|
445
514
|
this.denied.set(node, this.layoutGen);
|
|
446
515
|
this.animations.drop(node, true);
|
|
@@ -450,18 +519,29 @@ export class CocoaPromotion {
|
|
|
450
519
|
// Then the parts, against the layers this frame keeps: a node
|
|
451
520
|
// promoted over a part is a layer above it, not ink on it.
|
|
452
521
|
lifted = this.sprites.decide(root, offers);
|
|
453
|
-
//
|
|
522
|
+
// A thumb no layer is under any more goes back into the bitmap.
|
|
523
|
+
for (const pane of [...this.thumbs.keys()]) {
|
|
524
|
+
if (!wanted.has(pane)) this._dropThumbs(pane, root);
|
|
525
|
+
}
|
|
526
|
+
// One order for every layer above the bitmap, nodes, parts and
|
|
527
|
+
// thumbs alike.
|
|
454
528
|
const order = this._inPaintOrder([...this.promoted.keys()]);
|
|
455
529
|
for (const state of this.sprites.entries()) {
|
|
456
530
|
order.push({ state, key: state.order });
|
|
457
531
|
}
|
|
532
|
+
for (const pane of wanted) {
|
|
533
|
+
const key = this._paintKey(pane);
|
|
534
|
+
if (key) order.push({ pane, key: [...key, AFTER_CHILDREN] });
|
|
535
|
+
}
|
|
458
536
|
order.sort(byPaintKey);
|
|
459
537
|
for (let i = 0; i < order.length; i++) {
|
|
460
|
-
const { node, state } = order[i];
|
|
538
|
+
const { node, state, pane } = order[i];
|
|
461
539
|
if (node) this._sync(node, i, root, layoutRan);
|
|
540
|
+
else if (pane) this._syncThumbs(pane, i, root);
|
|
462
541
|
else this.sprites.sync(state, i);
|
|
463
542
|
}
|
|
464
543
|
} finally {
|
|
544
|
+
this._thumbsWanted = null;
|
|
465
545
|
native.txCommit();
|
|
466
546
|
}
|
|
467
547
|
// An element told which of its parts are on layers now may claim,
|
|
@@ -472,17 +552,55 @@ export class CocoaPromotion {
|
|
|
472
552
|
_mayStay(node, key) {
|
|
473
553
|
if (!key || !promotableNode(node)) return false;
|
|
474
554
|
if (this.releasing.has(node)) return false; // its grace ran out
|
|
475
|
-
|
|
555
|
+
const cut = { thumbs: new Set(), round: true };
|
|
556
|
+
const reach = reachOf(node);
|
|
557
|
+
if (!reach || !this._clear(node, reach, cut)) return false;
|
|
558
|
+
for (const pane of cut.thumbs) this._thumbsWanted?.add(pane);
|
|
559
|
+
this.promoted.get(node).clip = clipOf(cut, reach);
|
|
560
|
+
return true;
|
|
476
561
|
}
|
|
477
562
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Would a frame put `node` on a layer now? Asked of a loop the clock runs
|
|
565
|
+
* because a frame turned it down or gave it back (nodes/animation.js
|
|
566
|
+
* `_offerLoopsAgain`): the scene that refused it may have moved on — a
|
|
567
|
+
* fading ancestor arrived, a sibling went, a scroll took the thumb away —
|
|
568
|
+
* and nothing about the loop itself would ever ask again. The frame
|
|
569
|
+
* decides as it always does; this is what keeps a loop that would only
|
|
570
|
+
* be turned down again from being offered every frame.
|
|
571
|
+
*/
|
|
572
|
+
wouldTake(node) {
|
|
573
|
+
if (this.window.destroyed || node.destroyed) return false;
|
|
574
|
+
if (this.promoted.has(node)) return true;
|
|
575
|
+
if (!promotableNode(node) || !this._paintKey(node)) return false;
|
|
576
|
+
const reach = reachOf(node);
|
|
577
|
+
if (!reach) return false;
|
|
578
|
+
if (!this._clear(node, reach, { thumbs: new Set(), round: true })) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
// asked of the scene as it is now, so a refusal from an earlier one —
|
|
582
|
+
// kept for the layout it was made in, and a fade that arrives lays
|
|
583
|
+
// nothing out — no longer stands
|
|
584
|
+
this.denied.delete(node);
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_promote(node, root, clip = null) {
|
|
589
|
+
const p = {
|
|
590
|
+
visual: new Visual(this, node),
|
|
483
591
|
content: null,
|
|
484
592
|
dirty: { all: true, rects: [] },
|
|
485
|
-
|
|
593
|
+
// the clip a clipping ancestor cuts it to, or null (`clipOf`), the box
|
|
594
|
+
// that masks to it, and the box of the square clips around a rounded
|
|
595
|
+
// one
|
|
596
|
+
clip,
|
|
597
|
+
box: null,
|
|
598
|
+
boxProps: {},
|
|
599
|
+
outerBox: null,
|
|
600
|
+
outerProps: {},
|
|
601
|
+
};
|
|
602
|
+
this.promoted.set(node, p);
|
|
603
|
+
this._place(p);
|
|
486
604
|
node._promoted = true;
|
|
487
605
|
// the bitmap under it repaints without it, from this frame on — and so
|
|
488
606
|
// does the raster of a promoted node it is in, which the claim that
|
|
@@ -505,12 +623,64 @@ export class CocoaPromotion {
|
|
|
505
623
|
this.animations.drop(node, reclaim && !node.destroyed);
|
|
506
624
|
this._dropContent(p);
|
|
507
625
|
p.visual.destroy();
|
|
626
|
+
if (p.box) this.native.removeFromSuperlayer(p.outerBox ?? p.box);
|
|
508
627
|
if (!node.destroyed && node.abs) {
|
|
509
628
|
this._repaintHolder(node);
|
|
510
629
|
this._claim(root, node.paintBounds());
|
|
511
630
|
}
|
|
512
631
|
}
|
|
513
632
|
|
|
633
|
+
/**
|
|
634
|
+
* The node's layer on the window root, or in a box of its clip's size
|
|
635
|
+
* that masks to it, where a clipping ancestor with square corners cuts it
|
|
636
|
+
* (`clipOf`) — a row half scrolled out of its list, as an element's part
|
|
637
|
+
* is cut (src/cocoa/sprites.js). A layer moved into or out of the box
|
|
638
|
+
* sends everything again (`Visual.attach`).
|
|
639
|
+
*/
|
|
640
|
+
_place(p) {
|
|
641
|
+
const want = p.clip ? (p.clip.outer ? 2 : 1) : 0;
|
|
642
|
+
const have = p.outerBox ? 2 : p.box ? 1 : 0;
|
|
643
|
+
if (want === have) {
|
|
644
|
+
if (!want) p.visual.attach(this.rootVisual);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (p.box) {
|
|
648
|
+
p.visual.attach(this.rootVisual);
|
|
649
|
+
this.native.removeFromSuperlayer(p.outerBox ?? p.box);
|
|
650
|
+
p.box = null;
|
|
651
|
+
p.outerBox = null;
|
|
652
|
+
}
|
|
653
|
+
if (!want) return;
|
|
654
|
+
p.box = this.native.createLayer();
|
|
655
|
+
p.boxProps = {};
|
|
656
|
+
if (want === 2) {
|
|
657
|
+
p.outerBox = this.native.createLayer();
|
|
658
|
+
p.outerProps = {};
|
|
659
|
+
this.native.addSublayer(this.rootVisual.layer, p.outerBox);
|
|
660
|
+
this.native.addSublayer(p.outerBox, p.box);
|
|
661
|
+
} else {
|
|
662
|
+
this.native.addSublayer(this.rootVisual.layer, p.box);
|
|
663
|
+
}
|
|
664
|
+
p.visual.attach({ layer: p.box });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** A box's properties, sent where they changed. */
|
|
668
|
+
_setBox(layer, p, field, next) {
|
|
669
|
+
const was = p[field];
|
|
670
|
+
for (const key of Object.keys(next)) {
|
|
671
|
+
const a = was[key];
|
|
672
|
+
const b = next[key];
|
|
673
|
+
const same = Array.isArray(b)
|
|
674
|
+
? Array.isArray(a) && b.every((v, i) => v === a[i])
|
|
675
|
+
: a === b;
|
|
676
|
+
if (!same) {
|
|
677
|
+
this.native.setLayerProps(layer, next);
|
|
678
|
+
p[field] = next;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
514
684
|
/**
|
|
515
685
|
* A node coming off its layer inside a promoted node: the bitmap holds a
|
|
516
686
|
* hole there, and the node's pixels belong in that node's raster — so the
|
|
@@ -597,8 +767,9 @@ export class CocoaPromotion {
|
|
|
597
767
|
* it, and the parent must not fade: its group is drawn on the bitmap or on
|
|
598
768
|
* its own layer, and either way a layer of the node's would be outside it.
|
|
599
769
|
*
|
|
600
|
-
* With `cut`, the
|
|
601
|
-
*
|
|
770
|
+
* With `cut`, the layer can be cut to a rectangle — a node's
|
|
771
|
+
* (`clipOf`), or an element's part's (src/cocoa/sprites.js): a clipping
|
|
772
|
+
* ancestor with square
|
|
602
773
|
* corners narrows `cut.clip` to its padding box — its border is drawn
|
|
603
774
|
* over what it holds anyway — instead of having to hold the part whole,
|
|
604
775
|
* and every test from there up is asked of what shows through the clip.
|
|
@@ -638,6 +809,30 @@ export class CocoaPromotion {
|
|
|
638
809
|
return true;
|
|
639
810
|
}
|
|
640
811
|
}
|
|
812
|
+
// A node's layer may be cut to one rounded clip, the innermost that
|
|
813
|
+
// does not hold it — a block sliding along a bar's rounded track. Its
|
|
814
|
+
// padding box, with the corners its border leaves, and every square
|
|
815
|
+
// clip above it cuts it again as above (`outer`).
|
|
816
|
+
let roundCut = false;
|
|
817
|
+
if (
|
|
818
|
+
cut?.round &&
|
|
819
|
+
!cut.clip &&
|
|
820
|
+
!parent.isWindow &&
|
|
821
|
+
parent.clipsChildren?.() &&
|
|
822
|
+
typeof parent.style?.borderRadius === 'number' &&
|
|
823
|
+
parent.style.borderRadius > 0 &&
|
|
824
|
+
!containsRect(insetRect(parent.abs, parent.style.borderRadius), bounds)
|
|
825
|
+
) {
|
|
826
|
+
const border = borderReach(parent);
|
|
827
|
+
cut.clip = insetRect(parent.abs, border);
|
|
828
|
+
cut.radius = Math.max(0, parent.style.borderRadius - border);
|
|
829
|
+
roundCut = true;
|
|
830
|
+
bounds = intersectRects(bounds, cut.clip);
|
|
831
|
+
if (!bounds) {
|
|
832
|
+
cut.bounds = null;
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
641
836
|
const order = parent.paintOrder();
|
|
642
837
|
for (let j = order.indexOf(n) + 1; j < order.length; j++) {
|
|
643
838
|
if (this._reaches(order[j], bounds)) return false;
|
|
@@ -657,7 +852,7 @@ export class CocoaPromotion {
|
|
|
657
852
|
if (!containsRect(insetRect(parent.abs, inside), bounds))
|
|
658
853
|
return false;
|
|
659
854
|
}
|
|
660
|
-
if (parent.clipsChildren?.()) {
|
|
855
|
+
if (parent.clipsChildren?.() && !roundCut) {
|
|
661
856
|
const radius = parent.style?.borderRadius;
|
|
662
857
|
const clip =
|
|
663
858
|
typeof radius === 'number' && radius > 0
|
|
@@ -668,9 +863,12 @@ export class CocoaPromotion {
|
|
|
668
863
|
}
|
|
669
864
|
if (typeof parent._scrollbars === 'function') {
|
|
670
865
|
for (const bar of parent._scrollbars()) {
|
|
671
|
-
if (rectsOverlap(
|
|
672
|
-
|
|
673
|
-
|
|
866
|
+
if (!rectsOverlap(scrollbarInk(bar, this.scale), bounds)) continue;
|
|
867
|
+
// a thumb drawn over a node's layer goes on a layer of its own
|
|
868
|
+
// above it, where it can (`_thumbsLiftable`); an element's part
|
|
869
|
+
// keeps clear of it
|
|
870
|
+
if (!cut?.thumbs || !this._thumbsLiftable(parent)) return false;
|
|
871
|
+
cut.thumbs.add(parent);
|
|
674
872
|
}
|
|
675
873
|
}
|
|
676
874
|
}
|
|
@@ -697,12 +895,143 @@ export class CocoaPromotion {
|
|
|
697
895
|
return false;
|
|
698
896
|
}
|
|
699
897
|
|
|
898
|
+
// --- a pane's thumbs above the rows they are drawn over ----------------------
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Can `pane`'s thumbs go on a layer above everything inside it? The bars
|
|
902
|
+
* are painted after the pane's children, so a layer of theirs stands over
|
|
903
|
+
* the layers of the rows in the pane as the thumb stands over the rows —
|
|
904
|
+
* where nothing painted after the pane reaches anywhere the thumb can go,
|
|
905
|
+
* nothing above it fades, and every clip above it holds the track whole:
|
|
906
|
+
* `_clear` asked of the pane at its tracks. A pane that fades has its
|
|
907
|
+
* bars in its group.
|
|
908
|
+
*/
|
|
909
|
+
_thumbsLiftable(pane) {
|
|
910
|
+
if (fadesAsGroup(pane)) return false;
|
|
911
|
+
for (const bar of pane._scrollbars()) {
|
|
912
|
+
if (!this._clear(pane, scrollbarTrack(bar, this.scale))) return false;
|
|
913
|
+
}
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** `pane`'s thumbs on layers of their own at `z` among the layers above
|
|
918
|
+
* the bitmap, which stops painting them (`Scrollable._paintScrollbars`):
|
|
919
|
+
* a colour and a radius, as core paints one, where the thumb is now. */
|
|
920
|
+
_syncThumbs(pane, z, root) {
|
|
921
|
+
let layers = this.thumbs.get(pane);
|
|
922
|
+
if (!layers) {
|
|
923
|
+
layers = new Map();
|
|
924
|
+
this.thumbs.set(pane, layers);
|
|
925
|
+
pane._thumbsLifted = true;
|
|
926
|
+
// the bitmap under it, repainted without it
|
|
927
|
+
for (const bar of pane._scrollbars()) {
|
|
928
|
+
this._claim(root, scrollbarInk(bar, this.scale));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const s = this.scale;
|
|
932
|
+
const backgroundColor = this.app._parseColor(
|
|
933
|
+
String(pane.props.scrollbarColor || THUMB_COLOUR),
|
|
934
|
+
) ?? [0, 0, 0, 0.25];
|
|
935
|
+
const seen = new Set();
|
|
936
|
+
for (const bar of pane._scrollbars()) {
|
|
937
|
+
seen.add(bar.axis);
|
|
938
|
+
let entry = layers.get(bar.axis);
|
|
939
|
+
if (!entry) {
|
|
940
|
+
entry = { layer: this.native.createLayer(), props: {} };
|
|
941
|
+
this.native.addSublayer(this.rootVisual.layer, entry.layer);
|
|
942
|
+
layers.set(bar.axis, entry);
|
|
943
|
+
}
|
|
944
|
+
const next = {
|
|
945
|
+
frame: [bar.x / s, bar.y / s, bar.width / s, bar.height / s],
|
|
946
|
+
backgroundColor,
|
|
947
|
+
cornerRadius: 3,
|
|
948
|
+
zPosition: z,
|
|
949
|
+
};
|
|
950
|
+
const out = {};
|
|
951
|
+
let any = false;
|
|
952
|
+
for (const key of Object.keys(next)) {
|
|
953
|
+
const was = entry.props[key];
|
|
954
|
+
const value = next[key];
|
|
955
|
+
const same = Array.isArray(value)
|
|
956
|
+
? Array.isArray(was) && value.every((v, i) => v === was[i])
|
|
957
|
+
: was === value;
|
|
958
|
+
if (!same) {
|
|
959
|
+
out[key] = value;
|
|
960
|
+
any = true;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (any) {
|
|
964
|
+
this.native.setLayerProps(entry.layer, out);
|
|
965
|
+
entry.props = next;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
for (const [axis, entry] of layers) {
|
|
969
|
+
if (seen.has(axis)) continue;
|
|
970
|
+
this.native.removeFromSuperlayer(entry.layer);
|
|
971
|
+
layers.delete(axis);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** `pane`'s thumbs back into the bitmap, which paints them again where
|
|
976
|
+
* they are now. */
|
|
977
|
+
_dropThumbs(pane, root) {
|
|
978
|
+
const layers = this.thumbs.get(pane);
|
|
979
|
+
if (!layers) return;
|
|
980
|
+
this.thumbs.delete(pane);
|
|
981
|
+
for (const entry of layers.values()) {
|
|
982
|
+
this.native.removeFromSuperlayer(entry.layer);
|
|
983
|
+
}
|
|
984
|
+
pane._thumbsLifted = false;
|
|
985
|
+
if (pane.destroyed) return;
|
|
986
|
+
for (const bar of pane._scrollbars()) {
|
|
987
|
+
this._claim(root, scrollbarInk(bar, this.scale));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
700
991
|
// --- what the layer shows ----------------------------------------------------
|
|
701
992
|
|
|
702
993
|
_sync(node, order, root, layoutRan) {
|
|
703
994
|
const p = this.promoted.get(node);
|
|
704
|
-
|
|
705
|
-
|
|
995
|
+
this._place(p);
|
|
996
|
+
let origin = ORIGIN;
|
|
997
|
+
let z = order;
|
|
998
|
+
if (p.box) {
|
|
999
|
+
// the box stands at the node's place among the layers — or inside
|
|
1000
|
+
// the box of the square clips around it, which does — and the node's
|
|
1001
|
+
// layer is placed from the box's corner inside it
|
|
1002
|
+
const s = this.scale;
|
|
1003
|
+
const { rect: clip, radius, outer } = p.clip;
|
|
1004
|
+
let base = ORIGIN;
|
|
1005
|
+
let boxZ = order;
|
|
1006
|
+
if (p.outerBox) {
|
|
1007
|
+
this._setBox(p.outerBox, p, 'outerProps', {
|
|
1008
|
+
frame: [outer.x / s, outer.y / s, outer.width / s, outer.height / s],
|
|
1009
|
+
masksToBounds: true,
|
|
1010
|
+
zPosition: order,
|
|
1011
|
+
});
|
|
1012
|
+
base = outer;
|
|
1013
|
+
boxZ = 0;
|
|
1014
|
+
}
|
|
1015
|
+
this._setBox(p.box, p, 'boxProps', {
|
|
1016
|
+
frame: [
|
|
1017
|
+
(clip.x - base.x) / s,
|
|
1018
|
+
(clip.y - base.y) / s,
|
|
1019
|
+
clip.width / s,
|
|
1020
|
+
clip.height / s,
|
|
1021
|
+
],
|
|
1022
|
+
masksToBounds: true,
|
|
1023
|
+
// a circle's arc, as wide as the box allows, where the clip is
|
|
1024
|
+
// rounded
|
|
1025
|
+
cornerRadius: Math.min(radius, clip.width / 2, clip.height / 2) / s,
|
|
1026
|
+
zPosition: boxZ,
|
|
1027
|
+
});
|
|
1028
|
+
origin = clip;
|
|
1029
|
+
z = 0;
|
|
1030
|
+
}
|
|
1031
|
+
p.visual.set(propBoxProps(node, this.app, this.scale, origin, z));
|
|
1032
|
+
// after the model value went out, inside the same transaction: what
|
|
1033
|
+
// moves the node, as the layout has its parent now, then what waits
|
|
1034
|
+
this.animations.follow(node, p.visual.layer);
|
|
706
1035
|
this.animations.apply(node, p.visual.layer);
|
|
707
1036
|
this._syncContent(node, p, root, layoutRan);
|
|
708
1037
|
}
|
|
@@ -817,6 +1146,13 @@ export class CocoaPromotion {
|
|
|
817
1146
|
/** Everything off the root layer and freed: the window is going. */
|
|
818
1147
|
destroy() {
|
|
819
1148
|
this.sprites.destroy();
|
|
1149
|
+
for (const [pane, layers] of this.thumbs) {
|
|
1150
|
+
for (const entry of layers.values()) {
|
|
1151
|
+
this.native.removeFromSuperlayer(entry.layer);
|
|
1152
|
+
}
|
|
1153
|
+
pane._thumbsLifted = false;
|
|
1154
|
+
}
|
|
1155
|
+
this.thumbs.clear();
|
|
820
1156
|
for (const timer of this.idle.values()) clearTimeout(timer);
|
|
821
1157
|
this.idle.clear();
|
|
822
1158
|
this.releasing.clear();
|
|
@@ -825,6 +1161,7 @@ export class CocoaPromotion {
|
|
|
825
1161
|
this.animations.drop(node, false);
|
|
826
1162
|
this._dropContent(p);
|
|
827
1163
|
p.visual.destroy();
|
|
1164
|
+
if (p.box) this.native.removeFromSuperlayer(p.outerBox ?? p.box);
|
|
828
1165
|
}
|
|
829
1166
|
this.promoted.clear();
|
|
830
1167
|
this.candidates.clear();
|
package/src/cocoa/sprites.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import { RASTER_PAD, RasterState } from './presenter.js';
|
|
49
49
|
import { CocoaPlayer, PlayerLift } from './player.js';
|
|
50
50
|
import { VideoLift, canLiftVideo } from './video.js';
|
|
51
|
-
import { intersectRects, rectContains } from '../nodes/rects.js';
|
|
51
|
+
import { intersectRects, rectContains, rectsOverlap } from '../nodes/rects.js';
|
|
52
52
|
import { DEV } from '../nodes/util.js';
|
|
53
53
|
import { isVideoFrames } from '../videoframes.js';
|
|
54
54
|
|
|
@@ -497,6 +497,7 @@ export class SpriteLayers {
|
|
|
497
497
|
state.extent = shown;
|
|
498
498
|
now.set(sprite.key, state);
|
|
499
499
|
}
|
|
500
|
+
if (place) this._underDrawn(host, list, now, was, root);
|
|
500
501
|
if (was) {
|
|
501
502
|
for (const [key, state] of was) {
|
|
502
503
|
if (!now.has(key)) this._drop(state, root);
|
|
@@ -512,6 +513,44 @@ export class SpriteLayers {
|
|
|
512
513
|
return changed;
|
|
513
514
|
}
|
|
514
515
|
|
|
516
|
+
/**
|
|
517
|
+
* An element lists its parts in the order it paints them, and its layers
|
|
518
|
+
* stand in that order above the bitmap. A part this frame turned down is
|
|
519
|
+
* the element's to draw, in the bitmap, under every layer, so a part
|
|
520
|
+
* listed before it — painted under it — may not stay on a layer over it
|
|
521
|
+
* where the two meet: the later part's pixels would be under the earlier
|
|
522
|
+
* one's. An element may then offer a part that a later one of its own
|
|
523
|
+
* overlaps, on the word that the later one is lifted too, and this keeps
|
|
524
|
+
* that word. Walked from the end of the list, so a part given back here
|
|
525
|
+
* is under the ones before it in turn. One inside another's layer goes
|
|
526
|
+
* with that layer, and is not asked.
|
|
527
|
+
*/
|
|
528
|
+
_underDrawn(host, list, now, was, root) {
|
|
529
|
+
const drawn = []; // where later parts are in the bitmap
|
|
530
|
+
const giveBack = (state) => {
|
|
531
|
+
for (const kid of state.kids) giveBack(kid);
|
|
532
|
+
now.delete(state.key);
|
|
533
|
+
// one lifted in an earlier frame goes with the rest of those
|
|
534
|
+
if (was?.get(state.key) !== state) this._drop(state, root);
|
|
535
|
+
};
|
|
536
|
+
for (let index = list.length - 1; index >= 0; index--) {
|
|
537
|
+
const sprite = list[index];
|
|
538
|
+
if (!isSprite(sprite) || sprite.parent !== undefined) continue;
|
|
539
|
+
const state = now.get(sprite.key);
|
|
540
|
+
if (state?.sprite === sprite) {
|
|
541
|
+
if (!state.extent) continue; // it shows nothing to be over
|
|
542
|
+
if (!drawn.some((rect) => rectsOverlap(rect, state.extent))) continue;
|
|
543
|
+
giveBack(state);
|
|
544
|
+
drawn.push(state.extent);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
// the element draws it, where its own clip lets it show
|
|
548
|
+
const extent = extentOf(sprite);
|
|
549
|
+
const shows = sprite.clip ? intersectRects(extent, sprite.clip) : extent;
|
|
550
|
+
if (shows) drawn.push(shows);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
515
554
|
/** Every lifted part above the bitmap, with its place in the paint
|
|
516
555
|
* order: not one inside another's layer, which goes with it (`sync`). */
|
|
517
556
|
entries() {
|
package/src/cocoa/window.js
CHANGED
|
@@ -256,6 +256,10 @@ export class CocoaWindow {
|
|
|
256
256
|
this._promotion.animate(node, prop, entry);
|
|
257
257
|
this.cancelNodeAnimation = (node, prop) =>
|
|
258
258
|
this._promotion.cancel(node, prop);
|
|
259
|
+
// …and whether a loop the clock runs, turned down or given back by
|
|
260
|
+
// an earlier frame, would be taken now (nodes/animation.js
|
|
261
|
+
// `_offerLoopsAgain`)
|
|
262
|
+
this.wouldAnimateNode = (node) => this._promotion.wouldTake(node);
|
|
259
263
|
this.noteInvalidate = (damage, layoutChanged) =>
|
|
260
264
|
this._promotion.noteInvalidate(damage, layoutChanged);
|
|
261
265
|
this.prepareFrame = (root, layoutRan) =>
|
|
@@ -27,9 +27,9 @@ const CROSSING_MS = 1100;
|
|
|
27
27
|
* [styling.md](../../docs/styling.md#loops)), not a timer here: it runs on
|
|
28
28
|
* the window's own frame clock, claims the block as its damage every frame
|
|
29
29
|
* instead of invalidating the window, and stops itself when the window is
|
|
30
|
-
* unmapped, minimized or buried
|
|
31
|
-
* do none of those
|
|
32
|
-
* second to move a rectangle.
|
|
30
|
+
* unmapped, minimized or buried, or the bar is scrolled out of sight. A
|
|
31
|
+
* `setInterval` calling `setState` would do none of those, and would
|
|
32
|
+
* re-render this component sixty times a second to move a rectangle.
|
|
33
33
|
*
|
|
34
34
|
* The travel is in **percentages of the track**, so nothing here measures
|
|
35
35
|
* anything: the bar is right at whatever width its container gives it and
|
|
@@ -88,6 +88,10 @@ export function ProgressBar({
|
|
|
88
88
|
start: moving ? `-${BLOCK}` : '30%',
|
|
89
89
|
borderRadius: height / 2,
|
|
90
90
|
backgroundColor: color ?? theme.accent,
|
|
91
|
+
// Nothing to press: the bar is the track. Said so, a presenter
|
|
92
|
+
// that runs the slide as the block's position (macOS) may, since
|
|
93
|
+
// input would otherwise be hit where the layout left the block.
|
|
94
|
+
pointerEvents: 'none',
|
|
91
95
|
...(moving && {
|
|
92
96
|
animation: { start: { to: '100%', duration: CROSSING_MS } },
|
|
93
97
|
}),
|