react-x11 2.35.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 +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/types/system.d.ts +6 -0
package/package.json
CHANGED
package/src/backend/context2d.js
CHANGED
|
@@ -194,6 +194,19 @@ class LinearGradient {
|
|
|
194
194
|
|
|
195
195
|
const clamp01 = (v) => Math.min(1, Math.max(0, Number(v) || 0));
|
|
196
196
|
|
|
197
|
+
// What `textAlign`, `textBaseline` and `direction` take. A value outside
|
|
198
|
+
// them is ignored and the last one stays, as canvas has it.
|
|
199
|
+
const TEXT_ALIGNS = new Set(['start', 'end', 'left', 'right', 'center']);
|
|
200
|
+
const TEXT_BASELINES = new Set([
|
|
201
|
+
'top',
|
|
202
|
+
'hanging',
|
|
203
|
+
'middle',
|
|
204
|
+
'alphabetic',
|
|
205
|
+
'ideographic',
|
|
206
|
+
'bottom',
|
|
207
|
+
]);
|
|
208
|
+
const DIRECTIONS = new Set(['ltr', 'rtl', 'inherit']);
|
|
209
|
+
|
|
197
210
|
/**
|
|
198
211
|
* The Render ops text draws with, numbered as XRender numbers them so a
|
|
199
212
|
* caller's `ctx.Render?.PictOp?.Over ?? 3` reads the same on both
|
|
@@ -428,6 +441,9 @@ export class BackendContext2D {
|
|
|
428
441
|
dash: [],
|
|
429
442
|
dashOffset: 0,
|
|
430
443
|
font: '10px sans-serif',
|
|
444
|
+
textAlign: 'start',
|
|
445
|
+
textBaseline: 'alphabetic',
|
|
446
|
+
direction: 'inherit',
|
|
431
447
|
shadowBlur: 0,
|
|
432
448
|
shadowOffsetX: 0,
|
|
433
449
|
shadowOffsetY: 0,
|
|
@@ -654,6 +670,37 @@ export class BackendContext2D {
|
|
|
654
670
|
this._state.font = String(value);
|
|
655
671
|
}
|
|
656
672
|
|
|
673
|
+
/** Where `fillText`'s x is on the text: its start, end, left, right or
|
|
674
|
+
* centre. */
|
|
675
|
+
get textAlign() {
|
|
676
|
+
return this._state.textAlign;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
set textAlign(value) {
|
|
680
|
+
if (TEXT_ALIGNS.has(value)) this._state.textAlign = value;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Where `fillText`'s y is on the text: its alphabetic baseline, or the
|
|
684
|
+
* top, middle or bottom of its em box, or its hanging or ideographic
|
|
685
|
+
* baseline. */
|
|
686
|
+
get textBaseline() {
|
|
687
|
+
return this._state.textBaseline;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
set textBaseline(value) {
|
|
691
|
+
if (TEXT_BASELINES.has(value)) this._state.textBaseline = value;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Which way `start` and `end` face. `inherit`, with no element to take
|
|
695
|
+
* it from, is the text's own: right to left where it starts so. */
|
|
696
|
+
get direction() {
|
|
697
|
+
return this._state.direction;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
set direction(value) {
|
|
701
|
+
if (DIRECTIONS.has(value)) this._state.direction = value;
|
|
702
|
+
}
|
|
703
|
+
|
|
657
704
|
get shadowBlur() {
|
|
658
705
|
return this._state.shadowBlur;
|
|
659
706
|
}
|
|
@@ -1932,9 +1979,53 @@ export class BackendContext2D {
|
|
|
1932
1979
|
fillText(text, x, y) {
|
|
1933
1980
|
const layout = this._fontLayout(text);
|
|
1934
1981
|
if (!layout) return;
|
|
1935
|
-
// canvas fillText's y is the baseline
|
|
1936
|
-
|
|
1937
|
-
|
|
1982
|
+
// canvas fillText's y is on the baseline `textBaseline` names, and x on
|
|
1983
|
+
// the side `textAlign` does; a layout draws from its top left
|
|
1984
|
+
const line = layout.lines[0];
|
|
1985
|
+
const baseline = line?.baseline ?? 0;
|
|
1986
|
+
this._drawLayout(
|
|
1987
|
+
layout,
|
|
1988
|
+
x + this._alignOffset(layout, line),
|
|
1989
|
+
y - baseline + this._baselineOffset(line),
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/** How far left of x the text starts: ntk's `_alignOffset`, so a label
|
|
1994
|
+
* centred on X11 is centred here too. */
|
|
1995
|
+
_alignOffset(layout, line) {
|
|
1996
|
+
let align = this._state.textAlign;
|
|
1997
|
+
if (align === 'start' || align === 'end') {
|
|
1998
|
+
const direction = this._state.direction;
|
|
1999
|
+
const rtl =
|
|
2000
|
+
direction === 'rtl' ||
|
|
2001
|
+
(direction === 'inherit' &&
|
|
2002
|
+
!!line?.runs?.find((r) => r.start === 0)?.rtl);
|
|
2003
|
+
align = (align === 'start') !== rtl ? 'left' : 'right';
|
|
2004
|
+
}
|
|
2005
|
+
const width = layout.width ?? 0;
|
|
2006
|
+
if (align === 'center') return -width / 2;
|
|
2007
|
+
if (align === 'right') return -width;
|
|
2008
|
+
return 0;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/** How far below y the alphabetic baseline is: ntk's `_baselineOffset`,
|
|
2012
|
+
* from the face's ascent and descent. */
|
|
2013
|
+
_baselineOffset(line) {
|
|
2014
|
+
const ascent = line?.ascent ?? 0;
|
|
2015
|
+
const descent = line?.descent ?? 0;
|
|
2016
|
+
switch (this._state.textBaseline) {
|
|
2017
|
+
case 'top':
|
|
2018
|
+
return ascent;
|
|
2019
|
+
case 'hanging':
|
|
2020
|
+
return ascent * 0.8;
|
|
2021
|
+
case 'middle':
|
|
2022
|
+
return (ascent - descent) / 2;
|
|
2023
|
+
case 'bottom':
|
|
2024
|
+
case 'ideographic':
|
|
2025
|
+
return -descent;
|
|
2026
|
+
default:
|
|
2027
|
+
return 0;
|
|
2028
|
+
}
|
|
1938
2029
|
}
|
|
1939
2030
|
|
|
1940
2031
|
_fontLayout(text, color) {
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -29,7 +29,12 @@ import { cssColorStraight } from 'ntk/color';
|
|
|
29
29
|
import { Node } from '../nodes/node.js';
|
|
30
30
|
import { addDamageRect, damageToPaint } from '../nodes/damage.js';
|
|
31
31
|
import { intersectRects } from '../nodes/rects.js';
|
|
32
|
-
import {
|
|
32
|
+
import { now as animationNow } from '../nodes/animation.js';
|
|
33
|
+
import {
|
|
34
|
+
EASING_CONTROL_POINTS,
|
|
35
|
+
TRANSITION_CONTROL_POINTS,
|
|
36
|
+
resolveBorderWidths,
|
|
37
|
+
} from '../styles.js';
|
|
33
38
|
import { BackendContext2D } from '../backend/context2d.js';
|
|
34
39
|
|
|
35
40
|
export const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
@@ -361,6 +366,33 @@ const ANIMATED_KEY_PATHS = Object.freeze({
|
|
|
361
366
|
opacity: { keyPath: 'opacity' },
|
|
362
367
|
});
|
|
363
368
|
|
|
369
|
+
/**
|
|
370
|
+
* An inset of a node out of the flow moves that node and nothing else — no
|
|
371
|
+
* sibling, not its parent — so a loop on one is its layer's position: an
|
|
372
|
+
* offset added to where the layout put the node, which is at the inset's
|
|
373
|
+
* declared value while the render server runs the loop (the style is the
|
|
374
|
+
* model). Only for a node no pointer lands on (`pointerEvents: 'none'`),
|
|
375
|
+
* since input is hit against the layout's rect, which holds still: an
|
|
376
|
+
* indeterminate bar's block, a highlight that sweeps along a track. Which
|
|
377
|
+
* axis, and which way a larger value moves the layer; `start` and `end` are
|
|
378
|
+
* mirrored under RTL.
|
|
379
|
+
*/
|
|
380
|
+
const INSET_KEY_PATHS = Object.freeze({
|
|
381
|
+
left: { keyPath: 'position.x', axis: 'x', sign: 1 },
|
|
382
|
+
right: { keyPath: 'position.x', axis: 'x', sign: -1 },
|
|
383
|
+
start: { keyPath: 'position.x', axis: 'x', sign: 1, logical: true },
|
|
384
|
+
end: { keyPath: 'position.x', axis: 'x', sign: -1, logical: true },
|
|
385
|
+
top: { keyPath: 'position.y', axis: 'y', sign: 1 },
|
|
386
|
+
bottom: { keyPath: 'position.y', axis: 'y', sign: -1 },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/** Does a loop on `prop` move its node, where a presenter runs it
|
|
390
|
+
* (`INSET_KEY_PATHS`)? */
|
|
391
|
+
export const movesNode = (prop) => Object.hasOwn(INSET_KEY_PATHS, prop);
|
|
392
|
+
|
|
393
|
+
// a length or a share of the room: `12`, `'-40%'`
|
|
394
|
+
const PERCENT = /^\s*(-?(?:\d+\.?\d*|\.\d+))%\s*$/;
|
|
395
|
+
|
|
364
396
|
// the ids the bridge reports an animation's end under: unique per process,
|
|
365
397
|
// looked up per app (`_animationEnds`)
|
|
366
398
|
let animationSeq = 0;
|
|
@@ -544,12 +576,16 @@ export class LayerAnimations {
|
|
|
544
576
|
* is in its bitmap, not on its layer.
|
|
545
577
|
*/
|
|
546
578
|
take(node, prop, entry) {
|
|
547
|
-
const map = ANIMATED_KEY_PATHS[prop];
|
|
579
|
+
const map = ANIMATED_KEY_PATHS[prop] ?? INSET_KEY_PATHS[prop];
|
|
548
580
|
if (!map || !stylePaintsPlain(node, node._targetStyle ?? node.style)) {
|
|
549
581
|
return false;
|
|
550
582
|
}
|
|
551
|
-
if (
|
|
552
|
-
|
|
583
|
+
if (map.axis) {
|
|
584
|
+
if (!this._insetOffsets(node, prop, entry)) return false;
|
|
585
|
+
} else {
|
|
586
|
+
if (this._value(map, entry.from) == null) return false;
|
|
587
|
+
if (this._value(map, entry.to) == null) return false;
|
|
588
|
+
}
|
|
553
589
|
let pending = this.pending.get(node);
|
|
554
590
|
if (!pending) this.pending.set(node, (pending = new Map()));
|
|
555
591
|
pending.set(prop, entry);
|
|
@@ -559,7 +595,12 @@ export class LayerAnimations {
|
|
|
559
595
|
/** Stop what runs for `prop` on `node`: a loop the window lost sight of,
|
|
560
596
|
* a declaration that changed, a transition the clock takes back. */
|
|
561
597
|
cancel(node, prop) {
|
|
562
|
-
this.pending.get(node)
|
|
598
|
+
const pending = this.pending.get(node);
|
|
599
|
+
// …and nothing left waiting is no entry at all: an empty map still
|
|
600
|
+
// answers `has`, and a node with nothing to run went onto a layer it
|
|
601
|
+
// never gave back — a loop that went out of sight between its offer
|
|
602
|
+
// and its frame did
|
|
603
|
+
if (pending?.delete(prop) && pending.size === 0) this.pending.delete(node);
|
|
563
604
|
this._removeLive(node, prop);
|
|
564
605
|
}
|
|
565
606
|
|
|
@@ -585,6 +626,78 @@ export class LayerAnimations {
|
|
|
585
626
|
: null;
|
|
586
627
|
}
|
|
587
628
|
|
|
629
|
+
/**
|
|
630
|
+
* A loop on an inset of `node` as its layer's offsets from where the
|
|
631
|
+
* layout puts it, `[from, to]` in points — or null where it cannot be
|
|
632
|
+
* one: not a loop, a node in the flow or one a pointer can land on, an
|
|
633
|
+
* inset with no declared value to rest at, or a value that is neither a
|
|
634
|
+
* length nor a percentage. A percentage is a share of the parent's
|
|
635
|
+
* padding box, which is what the layout places a node out of the flow
|
|
636
|
+
* in, so the offsets change when the parent's size does (`follow`).
|
|
637
|
+
*/
|
|
638
|
+
_insetOffsets(node, prop, entry) {
|
|
639
|
+
const map = INSET_KEY_PATHS[prop];
|
|
640
|
+
const style = node._targetStyle ?? node.style ?? {};
|
|
641
|
+
if (!entry.loop || style.position !== 'absolute') return null;
|
|
642
|
+
if (style.pointerEvents !== 'none') return null;
|
|
643
|
+
const parent = node.parent;
|
|
644
|
+
if (!parent?.abs) return null;
|
|
645
|
+
const edges = resolveBorderWidths(parent.style ?? {}, parent.direction);
|
|
646
|
+
const room =
|
|
647
|
+
map.axis === 'x'
|
|
648
|
+
? parent.abs.width - edges.left - edges.right
|
|
649
|
+
: parent.abs.height - edges.top - edges.bottom;
|
|
650
|
+
const length = (value) => {
|
|
651
|
+
if (typeof value === 'number')
|
|
652
|
+
return Number.isFinite(value) ? value : null;
|
|
653
|
+
const share = typeof value === 'string' ? PERCENT.exec(value) : null;
|
|
654
|
+
return share ? (Number(share[1]) / 100) * room : null;
|
|
655
|
+
};
|
|
656
|
+
const rest = length(style[prop]);
|
|
657
|
+
const from = length(entry.from);
|
|
658
|
+
const to = length(entry.to);
|
|
659
|
+
if (rest === null || from === null || to === null) return null;
|
|
660
|
+
const sign = map.logical && node.direction === 'rtl' ? -map.sign : map.sign;
|
|
661
|
+
return [
|
|
662
|
+
(sign * (from - rest)) / this.scale,
|
|
663
|
+
(sign * (to - rest)) / this.scale,
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* The offsets of a loop moving `node` as the layout has them now: one
|
|
669
|
+
* whose parent changed size runs again where it has got to in its cycle
|
|
670
|
+
* (`headStart`), and one that cannot run there any more goes back to the
|
|
671
|
+
* clock. Before `apply`, which attaches what this hands back.
|
|
672
|
+
*/
|
|
673
|
+
follow(node, layer) {
|
|
674
|
+
const live = this.live.get(node);
|
|
675
|
+
if (!live) return;
|
|
676
|
+
for (const [id, run] of [...live]) {
|
|
677
|
+
if (!run.offsets) continue;
|
|
678
|
+
const offsets = this._insetOffsets(node, run.prop, run.entry);
|
|
679
|
+
if (
|
|
680
|
+
offsets &&
|
|
681
|
+
offsets[0] === run.offsets[0] &&
|
|
682
|
+
offsets[1] === run.offsets[1]
|
|
683
|
+
) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
live.delete(id);
|
|
687
|
+
this._ends().delete(id);
|
|
688
|
+
this.native.removeAnimation(layer, run.key);
|
|
689
|
+
if (!offsets) {
|
|
690
|
+
node._offloadEnded(run.prop, run.entry);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
run.entry.headStart = animationNow() - run.entry.start;
|
|
694
|
+
let pending = this.pending.get(node);
|
|
695
|
+
if (!pending) this.pending.set(node, (pending = new Map()));
|
|
696
|
+
pending.set(run.prop, run.entry);
|
|
697
|
+
}
|
|
698
|
+
if (live.size === 0) this.live.delete(node);
|
|
699
|
+
}
|
|
700
|
+
|
|
588
701
|
/** The frame's half: attach what `take` accepted to `layer`, after the
|
|
589
702
|
* model value went out, inside the same transaction. */
|
|
590
703
|
apply(node, layer) {
|
|
@@ -592,23 +705,43 @@ export class LayerAnimations {
|
|
|
592
705
|
if (!pending) return;
|
|
593
706
|
this.pending.delete(node);
|
|
594
707
|
for (const [prop, entry] of pending) {
|
|
595
|
-
const map = ANIMATED_KEY_PATHS[prop];
|
|
708
|
+
const map = ANIMATED_KEY_PATHS[prop] ?? INSET_KEY_PATHS[prop];
|
|
596
709
|
const id = `rx${++animationSeq}`;
|
|
597
710
|
const key = `${prop}:${id}`;
|
|
598
711
|
const opts = { duration: entry.duration / 1000, id };
|
|
712
|
+
let offsets = null;
|
|
713
|
+
if (map.axis) {
|
|
714
|
+
// an inset: the layer's offset from where the layout put it, added
|
|
715
|
+
// to its position — as the layout has the parent now, which may not
|
|
716
|
+
// be what it was at the swap
|
|
717
|
+
offsets = this._insetOffsets(node, prop, entry);
|
|
718
|
+
if (!offsets) {
|
|
719
|
+
node._offloadDeclined(prop, entry);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
599
723
|
if (entry.loop) {
|
|
600
724
|
// a loop replaces whatever ran for the property: its declaration
|
|
601
725
|
// changed, and a loop restarts from the top when it does
|
|
602
726
|
this._removeLive(node, prop);
|
|
603
|
-
|
|
604
|
-
|
|
727
|
+
if (offsets) {
|
|
728
|
+
[opts.from, opts.to] = offsets;
|
|
729
|
+
opts.additive = true;
|
|
730
|
+
} else {
|
|
731
|
+
opts.from = this._value(map, entry.from);
|
|
732
|
+
opts.to = this._value(map, entry.to);
|
|
733
|
+
}
|
|
605
734
|
opts.timing = EASING_CONTROL_POINTS[entry.easing];
|
|
606
735
|
opts.repeat = Infinity;
|
|
607
736
|
opts.autoreverse = entry.alternate;
|
|
608
737
|
// CA's two halves of one CSS delay: a begin time the layer waits
|
|
609
|
-
// for showing `from`, or a time offset that starts it that far in
|
|
610
|
-
|
|
611
|
-
|
|
738
|
+
// for showing `from`, or a time offset that starts it that far in.
|
|
739
|
+
// A loop the clock has been running comes over where it has got to
|
|
740
|
+
// (`headStart`, nodes/animation.js `_offerLoopsAgain`), which is
|
|
741
|
+
// the same delay that much shorter.
|
|
742
|
+
const wait = (entry.delay ?? 0) - (entry.headStart ?? 0);
|
|
743
|
+
if (wait > 0) opts.delay = wait / 1000;
|
|
744
|
+
else if (wait < 0) opts.timeOffset = -wait / 1000;
|
|
612
745
|
} else if (map.colour) {
|
|
613
746
|
// From where the pixels are — which is what "an interrupted
|
|
614
747
|
// transition reverses from where it got to" means here. A colour
|
|
@@ -636,7 +769,7 @@ export class LayerAnimations {
|
|
|
636
769
|
// looked up after the removals above, which may have pruned the map
|
|
637
770
|
let live = this.live.get(node);
|
|
638
771
|
if (!live) this.live.set(node, (live = new Map()));
|
|
639
|
-
live.set(id, { prop, key, entry });
|
|
772
|
+
live.set(id, { prop, key, entry, offsets });
|
|
640
773
|
this._ends().set(id, (ev) => this._animationEnded(node, id, ev));
|
|
641
774
|
}
|
|
642
775
|
}
|
|
@@ -983,7 +1116,9 @@ export class CocoaLayerPresenter {
|
|
|
983
1116
|
visual.set(
|
|
984
1117
|
propBoxProps(node, this.window.app, this.scale, parentOrigin, order),
|
|
985
1118
|
);
|
|
986
|
-
// after the model value went out, inside the same transaction
|
|
1119
|
+
// after the model value went out, inside the same transaction: what
|
|
1120
|
+
// moves the node, as the layout has its parent now, then what waits
|
|
1121
|
+
this.animations.follow(node, visual.layer);
|
|
987
1122
|
this.animations.apply(node, visual.layer);
|
|
988
1123
|
}
|
|
989
1124
|
|
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
|
}),
|
package/src/nodes/animation.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
interpolate,
|
|
13
13
|
ease,
|
|
14
14
|
isLayoutProp,
|
|
15
|
+
TEXT_LAYOUT_PROPS,
|
|
15
16
|
} from '../styles.js';
|
|
16
17
|
import { GRID_CONTAINER_PROPS, GRID_ITEM_PROPS } from '../grid.js';
|
|
17
18
|
import { isPlaced } from '../layouts.js';
|
|
@@ -19,9 +20,19 @@ import { desktopSettings, watchDesktopSettings } from '../desktopsettings.js';
|
|
|
19
20
|
import { watchWindowState, windowStateSnapshot } from '../windowstate.js';
|
|
20
21
|
import { shadowExtentOf } from './boxpaint.js';
|
|
21
22
|
import { inThemeWalk } from './cascade.js';
|
|
22
|
-
import { insetRect } from './rects.js';
|
|
23
|
+
import { insetRect, intersectRects, outerPixels } from './rects.js';
|
|
23
24
|
import { shallowEqual } from './util.js';
|
|
24
25
|
|
|
26
|
+
/** How often a loop the clock runs is offered to a presenter again, at
|
|
27
|
+
* most (`_recheckLoops`): a check is a walk up the node's ancestors and
|
|
28
|
+
* along what is painted after it, and the loop is a frame every refresh
|
|
29
|
+
* for as long as it waits. */
|
|
30
|
+
const REOFFER_MS = 250;
|
|
31
|
+
|
|
32
|
+
/** The loopable properties that reach past the node's box without moving
|
|
33
|
+
* anything: a ring that grows draws further out than the node did. */
|
|
34
|
+
const REACH_PROPS = new Set(['outlineWidth', 'outlineOffset']);
|
|
35
|
+
|
|
25
36
|
/** Two values of a grid property that lay out the same: an inline
|
|
26
37
|
* `gridTemplateAreas` array is a new array every render. */
|
|
27
38
|
const sameGridValue = (a, b) =>
|
|
@@ -299,16 +310,53 @@ export class NodeAnimation {
|
|
|
299
310
|
}
|
|
300
311
|
|
|
301
312
|
/** The presenter could not run `entry` after all — the node turned into a
|
|
302
|
-
* raster between the swap and the frame
|
|
303
|
-
* the top
|
|
313
|
+
* raster between the swap and the frame, or its layer went. A transition
|
|
314
|
+
* starts again from the top, since the property's declared start is
|
|
315
|
+
* where its pixels still are. A loop goes on where it is in its cycle:
|
|
316
|
+
* the render server ran it from `start` (`LayerAnimations.apply`), or it
|
|
317
|
+
* never left the clock, so a spinner given back does not jump. */
|
|
304
318
|
_offloadDeclined(prop, entry) {
|
|
305
319
|
if (this._anim?.get(prop) !== entry || this.destroyed) return;
|
|
306
320
|
entry.offloaded = false;
|
|
307
|
-
entry.
|
|
321
|
+
entry.headStart = undefined;
|
|
322
|
+
if (entry.loop) entry.value = animationValueAt(entry, now() - entry.start);
|
|
323
|
+
else entry.start = now();
|
|
308
324
|
this.style = { ...this._targetStyle, ...this._animatedValues() };
|
|
309
325
|
this.root?._startAnimating(this);
|
|
310
326
|
}
|
|
311
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Loops the frame clock runs that the presenter would take now: turned
|
|
330
|
+
* down by a frame, or given back, by a scene that has changed since — an
|
|
331
|
+
* ancestor done fading, a sibling gone, a thumb scrolled away. Nothing
|
|
332
|
+
* about a loop would ever ask again, so the window asks (`_recheckLoops`),
|
|
333
|
+
* and only where the presenter says a frame would take it
|
|
334
|
+
* (`wouldAnimateNode`), so a loop it would turn down again costs no swap.
|
|
335
|
+
* Handed over where it is in its cycle (`headStart`).
|
|
336
|
+
*/
|
|
337
|
+
_offerLoopsAgain() {
|
|
338
|
+
const wnd = this.root?.window;
|
|
339
|
+
let asked = false;
|
|
340
|
+
let moved = false;
|
|
341
|
+
for (const [prop, a] of this._anim ?? []) {
|
|
342
|
+
if (!a.loop || a.offloaded) continue;
|
|
343
|
+
if (!asked) {
|
|
344
|
+
if (!wnd.wouldAnimateNode(this)) return;
|
|
345
|
+
asked = true;
|
|
346
|
+
}
|
|
347
|
+
a.headStart = now() - a.start;
|
|
348
|
+
if (this._offload(prop, a)) {
|
|
349
|
+
a.offloaded = true;
|
|
350
|
+
moved = true;
|
|
351
|
+
} else {
|
|
352
|
+
a.headStart = undefined;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!moved) return;
|
|
356
|
+
this.style = { ...this._targetStyle, ...this._animatedValues() };
|
|
357
|
+
this._scheduleAnimationFrames();
|
|
358
|
+
}
|
|
359
|
+
|
|
312
360
|
/** Keep the frame clock running only for what the clock itself animates;
|
|
313
361
|
* an offloaded-only node needs one frame — the one that sends the model
|
|
314
362
|
* and the animation — and not a loop of them. */
|
|
@@ -383,13 +431,19 @@ export class NodeAnimation {
|
|
|
383
431
|
// re-rendered — which is the frame after every state change in the
|
|
384
432
|
// app.
|
|
385
433
|
if (current?.loop && sameAnimation(current, spec)) {
|
|
386
|
-
// A loop the clock
|
|
387
|
-
// one declared at mount runs from `_setRoot`, before
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
434
|
+
// A loop the clock runs moves over the first time a presenter
|
|
435
|
+
// takes it: one declared at mount runs from `_setRoot`, before
|
|
436
|
+
// `realize`, and one a frame turned down runs until the next swap
|
|
437
|
+
// offers it again (or `_offerLoopsAgain` does). It goes where it
|
|
438
|
+
// has got to in its cycle (`headStart`), so nothing jumps.
|
|
439
|
+
if (!current.offloaded) {
|
|
440
|
+
current.headStart = now() - current.start;
|
|
441
|
+
if (this._offload(spec.prop, current)) {
|
|
442
|
+
current.offloaded = true;
|
|
443
|
+
changed = true;
|
|
444
|
+
} else {
|
|
445
|
+
current.headStart = undefined;
|
|
446
|
+
}
|
|
393
447
|
}
|
|
394
448
|
continue;
|
|
395
449
|
}
|
|
@@ -443,7 +497,76 @@ export class NodeAnimation {
|
|
|
443
497
|
return false;
|
|
444
498
|
}
|
|
445
499
|
if (desktopSettings(root.app).animations === false) return false;
|
|
446
|
-
|
|
500
|
+
if (this._hiddenInTree()) return false;
|
|
501
|
+
// last, since it is the one that measures — and a hidden subtree's
|
|
502
|
+
// rects are wherever it was when it went
|
|
503
|
+
this._outOfSight = this._loopsOutOfSight();
|
|
504
|
+
return !this._outOfSight;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Whether nothing this node's loops draw can be seen: all of it is cut
|
|
509
|
+
* away by the boxes that clip it, or lies outside the window — a spinner
|
|
510
|
+
* in a row scrolled out of its list, a bar in a panel scrolled away.
|
|
511
|
+
* Judged against the arrangement the last layout pass left, and again
|
|
512
|
+
* after every pass that moved anything (`_recheckLoops`).
|
|
513
|
+
*
|
|
514
|
+
* Never from where a frame of the loop has the node *now*: a block that
|
|
515
|
+
* slides across a track is off it at the start of every crossing, and a
|
|
516
|
+
* loop stopped there would rest there, unseen, for good. What is judged is
|
|
517
|
+
* what holds every frame (`_loopReach`), and a loop whose frames can move
|
|
518
|
+
* what is around it is never judged out of sight at all.
|
|
519
|
+
*/
|
|
520
|
+
_loopsOutOfSight() {
|
|
521
|
+
const window = this.root?.abs;
|
|
522
|
+
if (!this._placed || !window) return false;
|
|
523
|
+
const reach = this._loopReach();
|
|
524
|
+
if (!reach) return false;
|
|
525
|
+
let out = reach.rect;
|
|
526
|
+
for (let n = reach.from.parent; n && out; n = n.parent) {
|
|
527
|
+
if (n.isWindow) break;
|
|
528
|
+
if (n.clipsChildren()) out = intersectRects(out, outerPixels(n.abs));
|
|
529
|
+
}
|
|
530
|
+
return (
|
|
531
|
+
!out ||
|
|
532
|
+
!intersectRects(out, {
|
|
533
|
+
x: 0,
|
|
534
|
+
y: 0,
|
|
535
|
+
width: window.width,
|
|
536
|
+
height: window.height,
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* What every frame of this node's loops stays inside, as `{ rect, from }`
|
|
543
|
+
* — `from` is the node whose clipping ancestors cut `rect` — or null when
|
|
544
|
+
* nothing short of the window holds it.
|
|
545
|
+
*
|
|
546
|
+
* A loop on a colour, an opacity or a radius moves nothing and grows
|
|
547
|
+
* nothing, so it draws where the node and what it holds already reach.
|
|
548
|
+
* One that grows the node's outline, or moves or sizes a node out of the
|
|
549
|
+
* flow, can put it anywhere in the nearest box that clips it — and nowhere
|
|
550
|
+
* else, since a node out of the flow sizes nothing around it; with no such
|
|
551
|
+
* box, anywhere in the window. One that moves or sizes a node in the flow
|
|
552
|
+
* moves its siblings and can resize every box above it, the clipping ones
|
|
553
|
+
* among them, so no box holds it.
|
|
554
|
+
*/
|
|
555
|
+
_loopReach() {
|
|
556
|
+
let inPlace = true;
|
|
557
|
+
for (const { prop } of this._loops ?? []) {
|
|
558
|
+
if (REACH_PROPS.has(prop)) {
|
|
559
|
+
inPlace = false;
|
|
560
|
+
} else if (isLayoutProp(prop) || TEXT_LAYOUT_PROPS.has(prop)) {
|
|
561
|
+
if (this.style?.position !== 'absolute') return null;
|
|
562
|
+
inPlace = false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (inPlace) return { rect: this.paintBounds(), from: this };
|
|
566
|
+
for (let n = this.parent; n && !n.isWindow; n = n.parent) {
|
|
567
|
+
if (n.clipsChildren()) return { rect: outerPixels(n.abs), from: n };
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
447
570
|
}
|
|
448
571
|
|
|
449
572
|
/** Whether anything between this node and its window has taken it off the
|
|
@@ -579,6 +702,42 @@ export class WindowAnimation {
|
|
|
579
702
|
for (const node of [...this._loopNodes]) node._updateLoops();
|
|
580
703
|
}
|
|
581
704
|
|
|
705
|
+
/**
|
|
706
|
+
* After the frame's layout and placements, before the presenter's word.
|
|
707
|
+
*
|
|
708
|
+
* Where a pass moved things (`moved`), a loop it put out of sight stops,
|
|
709
|
+
* and one it brought back runs again (`_loopsOutOfSight`) — from the top,
|
|
710
|
+
* as every stop here resumes, since nobody saw where it had got to. Only
|
|
711
|
+
* the loops whose answer changed are asked again, so a pass that moved
|
|
712
|
+
* nothing of theirs costs a walk up from each.
|
|
713
|
+
*
|
|
714
|
+
* And where a presenter can run a loop in the render server, a loop in
|
|
715
|
+
* sight that the clock runs is offered to it again (`_offerLoopsAgain`),
|
|
716
|
+
* each at most every `REOFFER_MS`: what turned it down can go without a
|
|
717
|
+
* layout pass — an ancestor's fade arriving is a paint — and the loop is
|
|
718
|
+
* a frame every refresh until it is taken.
|
|
719
|
+
*/
|
|
720
|
+
_recheckLoops(moved) {
|
|
721
|
+
const offer = typeof this.window?.wouldAnimateNode === 'function';
|
|
722
|
+
if (!moved && !offer) return;
|
|
723
|
+
const t = offer ? now() : 0;
|
|
724
|
+
for (const node of [...this._loopNodes]) {
|
|
725
|
+
if (moved) {
|
|
726
|
+
const out = node._loopsOutOfSight();
|
|
727
|
+
if (out !== (node._outOfSight === true)) {
|
|
728
|
+
node._outOfSight = out;
|
|
729
|
+
node._updateLoops();
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (!offer || node._outOfSight === true || node.destroyed) continue;
|
|
734
|
+
if (!(t - (node._offeredAt ?? -Infinity) >= REOFFER_MS)) continue;
|
|
735
|
+
if (!node.root?._animating.has(node)) continue; // the clock runs none
|
|
736
|
+
node._offeredAt = t;
|
|
737
|
+
node._offerLoopsAgain();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
582
741
|
/** A node in this window started a transition. */
|
|
583
742
|
_startAnimating(node) {
|
|
584
743
|
this._animating.add(node);
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -950,6 +950,9 @@ export const Scrollable = (Base) =>
|
|
|
950
950
|
* its own route, since it paints through `_paintRegion` and never
|
|
951
951
|
* through `Node.paint`. */
|
|
952
952
|
_paintScrollbars(ctx) {
|
|
953
|
+
// on layers of their own, over the layers of the rows they are drawn
|
|
954
|
+
// over (src/cocoa/promotion.js `_syncThumbs`)
|
|
955
|
+
if (this._thumbsLifted) return;
|
|
953
956
|
for (const bar of this._scrollbars()) {
|
|
954
957
|
paintScrollbarThumb(ctx, bar, this.props.scrollbarColor);
|
|
955
958
|
}
|
|
@@ -286,9 +286,18 @@ export class WindowFlush {
|
|
|
286
286
|
}
|
|
287
287
|
// An animated placement asked for this frame and nothing laid out: its
|
|
288
288
|
// pass runs on its own, against the arrangement the last one left
|
|
289
|
+
let placed = false;
|
|
289
290
|
if (!layoutRan && this._placementsDue && this._placeNodes()) {
|
|
290
291
|
this.needsPaint = true;
|
|
292
|
+
placed = true;
|
|
291
293
|
}
|
|
294
|
+
// A loop this arrangement put out of sight stops, and one it brought
|
|
295
|
+
// back runs again; one the clock runs is offered to the presenter
|
|
296
|
+
// again (`_recheckLoops`). After the pass and the placements, so it is
|
|
297
|
+
// judged where things landed, and before the presenter's word, so a
|
|
298
|
+
// loop handed to it, or taken back, is in this frame. Nothing moves
|
|
299
|
+
// anything but these two passes, so sight is judged only after one.
|
|
300
|
+
if (this._loopNodes.size) this._recheckLoops(layoutRan || placed);
|
|
292
301
|
// A presenter compositing part of the tree on layers of its own — the
|
|
293
302
|
// surface presenter's promoted nodes (src/cocoa/promotion.js) — gets
|
|
294
303
|
// its word in here: after layout, so it sees where everything landed,
|
package/src/types/system.d.ts
CHANGED
|
@@ -247,6 +247,12 @@ export interface DesktopSettings {
|
|
|
247
247
|
readonly doubleClickDistance: number;
|
|
248
248
|
/** How far a press moves before it is a drag rather than a click. */
|
|
249
249
|
readonly dragThreshold: number;
|
|
250
|
+
/** **False means hold still** — the desktop asked for less motion:
|
|
251
|
+
* `Gtk/EnableAnimations` off over XSETTINGS, reduce motion on macOS.
|
|
252
|
+
* Core's loops do not start then (docs/styling.md, "Loops"); an element
|
|
253
|
+
* that animates on a timer of its own holds still too, and draws its
|
|
254
|
+
* resting frame. True wherever the desktop said nothing. */
|
|
255
|
+
readonly animations: boolean;
|
|
250
256
|
/** `'xsettings'`; `'macos'` on the Cocoa backend, where only `animations`
|
|
251
257
|
* comes from the system (reduce motion) and the rest are the defaults; or
|
|
252
258
|
* null where no settings daemon answered and these are the renderer's own
|