react-x11 2.34.0 → 2.36.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/backend/context2d.js +94 -3
- package/src/cocoa/presenter.js +148 -13
- package/src/cocoa/promotion.js +391 -39
- package/src/cocoa/sprites.js +168 -19
- 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/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
|
|
@@ -79,6 +92,11 @@ const rectsOverlap = (a, b) =>
|
|
|
79
92
|
a.y < b.y + b.height &&
|
|
80
93
|
b.y < a.y + a.height;
|
|
81
94
|
|
|
95
|
+
// `a` cut to `b`: their overlap, or a clip of no size where they have none,
|
|
96
|
+
// which hides what it holds rather than holding it whole
|
|
97
|
+
const cutTo = (a, b) =>
|
|
98
|
+
intersectRects(a, b) ?? { x: b.x, y: b.y, width: 0, height: 0 };
|
|
99
|
+
|
|
82
100
|
const containsRect = (outer, inner) =>
|
|
83
101
|
inner.x >= outer.x &&
|
|
84
102
|
inner.y >= outer.y &&
|
|
@@ -202,9 +220,23 @@ function promotableNode(node) {
|
|
|
202
220
|
return !paintsOutline(node);
|
|
203
221
|
}
|
|
204
222
|
|
|
205
|
-
/**
|
|
206
|
-
* antialiasing needs.
|
|
207
|
-
|
|
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) {
|
|
208
240
|
const pad = Math.ceil(2 * scale);
|
|
209
241
|
return bar.axis === 'x'
|
|
210
242
|
? {
|
|
@@ -221,6 +253,38 @@ function scrollbarStrip(bar, scale) {
|
|
|
221
253
|
};
|
|
222
254
|
}
|
|
223
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
|
+
|
|
224
288
|
/**
|
|
225
289
|
* The surface window's promoted nodes: which ones have a layer, what each
|
|
226
290
|
* layer shows, and the animations the render server runs on them.
|
|
@@ -258,6 +322,11 @@ export class CocoaPromotion {
|
|
|
258
322
|
// the parts of elements' drawing they offer (`Node.sprites()`), each on
|
|
259
323
|
// a layer of its own under the same rules (src/cocoa/sprites.js)
|
|
260
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;
|
|
261
330
|
this._claiming = false;
|
|
262
331
|
}
|
|
263
332
|
|
|
@@ -412,12 +481,14 @@ export class CocoaPromotion {
|
|
|
412
481
|
this.promoted.size === 0 &&
|
|
413
482
|
this.candidates.size === 0 &&
|
|
414
483
|
offers.size === 0 &&
|
|
415
|
-
this.sprites.hosts.size === 0
|
|
484
|
+
this.sprites.hosts.size === 0 &&
|
|
485
|
+
this.thumbs.size === 0
|
|
416
486
|
) {
|
|
417
487
|
return;
|
|
418
488
|
}
|
|
419
489
|
const native = this.native;
|
|
420
490
|
let lifted = null;
|
|
491
|
+
const wanted = (this._thumbsWanted = new Set());
|
|
421
492
|
native.txBegin({ disableActions: true });
|
|
422
493
|
try {
|
|
423
494
|
// Later-painted first: a node painted after this one that is coming
|
|
@@ -434,8 +505,11 @@ export class CocoaPromotion {
|
|
|
434
505
|
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
435
506
|
const { node, key } = candidates[i];
|
|
436
507
|
if (this.promoted.has(node) || !this.animations.has(node)) continue;
|
|
437
|
-
|
|
438
|
-
|
|
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));
|
|
439
513
|
} else {
|
|
440
514
|
this.denied.set(node, this.layoutGen);
|
|
441
515
|
this.animations.drop(node, true);
|
|
@@ -445,18 +519,29 @@ export class CocoaPromotion {
|
|
|
445
519
|
// Then the parts, against the layers this frame keeps: a node
|
|
446
520
|
// promoted over a part is a layer above it, not ink on it.
|
|
447
521
|
lifted = this.sprites.decide(root, offers);
|
|
448
|
-
//
|
|
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.
|
|
449
528
|
const order = this._inPaintOrder([...this.promoted.keys()]);
|
|
450
529
|
for (const state of this.sprites.entries()) {
|
|
451
530
|
order.push({ state, key: state.order });
|
|
452
531
|
}
|
|
532
|
+
for (const pane of wanted) {
|
|
533
|
+
const key = this._paintKey(pane);
|
|
534
|
+
if (key) order.push({ pane, key: [...key, AFTER_CHILDREN] });
|
|
535
|
+
}
|
|
453
536
|
order.sort(byPaintKey);
|
|
454
537
|
for (let i = 0; i < order.length; i++) {
|
|
455
|
-
const { node, state } = order[i];
|
|
538
|
+
const { node, state, pane } = order[i];
|
|
456
539
|
if (node) this._sync(node, i, root, layoutRan);
|
|
540
|
+
else if (pane) this._syncThumbs(pane, i, root);
|
|
457
541
|
else this.sprites.sync(state, i);
|
|
458
542
|
}
|
|
459
543
|
} finally {
|
|
544
|
+
this._thumbsWanted = null;
|
|
460
545
|
native.txCommit();
|
|
461
546
|
}
|
|
462
547
|
// An element told which of its parts are on layers now may claim,
|
|
@@ -467,17 +552,55 @@ export class CocoaPromotion {
|
|
|
467
552
|
_mayStay(node, key) {
|
|
468
553
|
if (!key || !promotableNode(node)) return false;
|
|
469
554
|
if (this.releasing.has(node)) return false; // its grace ran out
|
|
470
|
-
|
|
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;
|
|
471
561
|
}
|
|
472
562
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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),
|
|
478
591
|
content: null,
|
|
479
592
|
dirty: { all: true, rects: [] },
|
|
480
|
-
|
|
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);
|
|
481
604
|
node._promoted = true;
|
|
482
605
|
// the bitmap under it repaints without it, from this frame on — and so
|
|
483
606
|
// does the raster of a promoted node it is in, which the claim that
|
|
@@ -500,12 +623,64 @@ export class CocoaPromotion {
|
|
|
500
623
|
this.animations.drop(node, reclaim && !node.destroyed);
|
|
501
624
|
this._dropContent(p);
|
|
502
625
|
p.visual.destroy();
|
|
626
|
+
if (p.box) this.native.removeFromSuperlayer(p.outerBox ?? p.box);
|
|
503
627
|
if (!node.destroyed && node.abs) {
|
|
504
628
|
this._repaintHolder(node);
|
|
505
629
|
this._claim(root, node.paintBounds());
|
|
506
630
|
}
|
|
507
631
|
}
|
|
508
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
|
+
|
|
509
684
|
/**
|
|
510
685
|
* A node coming off its layer inside a promoted node: the bitmap holds a
|
|
511
686
|
* hole there, and the node's pixels belong in that node's raster — so the
|
|
@@ -592,8 +767,9 @@ export class CocoaPromotion {
|
|
|
592
767
|
* it, and the parent must not fade: its group is drawn on the bitmap or on
|
|
593
768
|
* its own layer, and either way a layer of the node's would be outside it.
|
|
594
769
|
*
|
|
595
|
-
* With `cut`, the
|
|
596
|
-
*
|
|
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
|
|
597
773
|
* corners narrows `cut.clip` to its padding box — its border is drawn
|
|
598
774
|
* over what it holds anyway — instead of having to hold the part whole,
|
|
599
775
|
* and every test from there up is asked of what shows through the clip.
|
|
@@ -615,13 +791,47 @@ export class CocoaPromotion {
|
|
|
615
791
|
!(parent.style?.borderRadius > 0)
|
|
616
792
|
) {
|
|
617
793
|
const inner = insetRect(parent.abs, borderReach(parent));
|
|
618
|
-
// a clip with round corners cut again is no box's shape
|
|
619
|
-
|
|
620
|
-
|
|
794
|
+
// a clip with round corners cut again is no one box's shape: the
|
|
795
|
+
// square ones go in a box of their own around it (`outer`)
|
|
796
|
+
if (cut.radius > 0 && cut.clip) {
|
|
797
|
+
if (cut.outer || !containsRect(inner, cut.clip)) {
|
|
798
|
+
cut.outer = cut.outer ? cutTo(cut.outer, inner) : inner;
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
cut.clip = cut.clip ? cutTo(cut.clip, inner) : inner;
|
|
802
|
+
}
|
|
803
|
+
bounds = intersectRects(bounds, cut.clip);
|
|
804
|
+
if (bounds && cut.outer) bounds = intersectRects(bounds, cut.outer);
|
|
805
|
+
// nothing of the part shows: its layer is kept, cut to show nothing,
|
|
806
|
+
// and there is nothing it could be under or over until it does
|
|
807
|
+
if (!bounds) {
|
|
808
|
+
cut.bounds = null;
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
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;
|
|
621
834
|
}
|
|
622
|
-
cut.clip = cut.clip ? intersectRects(cut.clip, inner) : inner;
|
|
623
|
-
bounds = cut.clip && intersectRects(bounds, cut.clip);
|
|
624
|
-
if (!bounds) return false;
|
|
625
835
|
}
|
|
626
836
|
const order = parent.paintOrder();
|
|
627
837
|
for (let j = order.indexOf(n) + 1; j < order.length; j++) {
|
|
@@ -642,7 +852,7 @@ export class CocoaPromotion {
|
|
|
642
852
|
if (!containsRect(insetRect(parent.abs, inside), bounds))
|
|
643
853
|
return false;
|
|
644
854
|
}
|
|
645
|
-
if (parent.clipsChildren?.()) {
|
|
855
|
+
if (parent.clipsChildren?.() && !roundCut) {
|
|
646
856
|
const radius = parent.style?.borderRadius;
|
|
647
857
|
const clip =
|
|
648
858
|
typeof radius === 'number' && radius > 0
|
|
@@ -653,9 +863,12 @@ export class CocoaPromotion {
|
|
|
653
863
|
}
|
|
654
864
|
if (typeof parent._scrollbars === 'function') {
|
|
655
865
|
for (const bar of parent._scrollbars()) {
|
|
656
|
-
if (rectsOverlap(
|
|
657
|
-
|
|
658
|
-
|
|
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);
|
|
659
872
|
}
|
|
660
873
|
}
|
|
661
874
|
}
|
|
@@ -682,12 +895,143 @@ export class CocoaPromotion {
|
|
|
682
895
|
return false;
|
|
683
896
|
}
|
|
684
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
|
+
|
|
685
991
|
// --- what the layer shows ----------------------------------------------------
|
|
686
992
|
|
|
687
993
|
_sync(node, order, root, layoutRan) {
|
|
688
994
|
const p = this.promoted.get(node);
|
|
689
|
-
|
|
690
|
-
|
|
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);
|
|
691
1035
|
this.animations.apply(node, p.visual.layer);
|
|
692
1036
|
this._syncContent(node, p, root, layoutRan);
|
|
693
1037
|
}
|
|
@@ -802,6 +1146,13 @@ export class CocoaPromotion {
|
|
|
802
1146
|
/** Everything off the root layer and freed: the window is going. */
|
|
803
1147
|
destroy() {
|
|
804
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();
|
|
805
1156
|
for (const timer of this.idle.values()) clearTimeout(timer);
|
|
806
1157
|
this.idle.clear();
|
|
807
1158
|
this.releasing.clear();
|
|
@@ -810,6 +1161,7 @@ export class CocoaPromotion {
|
|
|
810
1161
|
this.animations.drop(node, false);
|
|
811
1162
|
this._dropContent(p);
|
|
812
1163
|
p.visual.destroy();
|
|
1164
|
+
if (p.box) this.native.removeFromSuperlayer(p.outerBox ?? p.box);
|
|
813
1165
|
}
|
|
814
1166
|
this.promoted.clear();
|
|
815
1167
|
this.candidates.clear();
|