@ng-native/fabric 0.8.0 → 0.10.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/dist/css.d.ts +106 -4
- package/dist/css.js +444 -57
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +127 -9
- package/dist/engine.js +564 -82
- package/dist/engine.js.map +1 -1
- package/dist/transition.d.ts +11 -0
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* because a persistent parent holds references to specific child handles. That bubble is
|
|
8
8
|
* inherent and is what React's own Fabric renderer does.
|
|
9
9
|
*/
|
|
10
|
-
import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, placeReadElsewhere, siblingReach, } from "./css.js";
|
|
10
|
+
import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, placeReadElsewhere, siblingReach, } from "./css.js";
|
|
11
11
|
import { applyAria } from "./aria-props.js";
|
|
12
12
|
import { STYLED_ELEMENTS } from "./element-styles.js";
|
|
13
13
|
import { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
|
|
@@ -28,7 +28,7 @@ function animationOf(props) {
|
|
|
28
28
|
let spec = props['$animation'];
|
|
29
29
|
if (spec !== undefined)
|
|
30
30
|
delete props['$animation'];
|
|
31
|
-
for (const [key, field] of
|
|
31
|
+
for (const [key, field] of ANIMATION_ENTRIES) {
|
|
32
32
|
if (!(key in props))
|
|
33
33
|
continue;
|
|
34
34
|
const value = props[key];
|
|
@@ -47,6 +47,8 @@ const ANIMATION_PARTS = {
|
|
|
47
47
|
$animationFill: 'fill',
|
|
48
48
|
$playState: 'paused',
|
|
49
49
|
};
|
|
50
|
+
/** Listed once: `animationOf` reads them for every node that commits. */
|
|
51
|
+
const ANIMATION_ENTRIES = Object.entries(ANIMATION_PARTS);
|
|
50
52
|
function animationPart(field, value) {
|
|
51
53
|
switch (field) {
|
|
52
54
|
case 'iterations':
|
|
@@ -537,8 +539,8 @@ function lineBaseline(props, line, [, top, , bottom], fontScale) {
|
|
|
537
539
|
* out on both platforms: on Android, `EditText` centres a line box taller than the font's own
|
|
538
540
|
* about 1.7pt high of centre in a 44pt field, and centres the font's own exactly. A percentage
|
|
539
541
|
* height sizes the field only where its parent's height is definite, which is known at layout
|
|
540
|
-
* alone, so such a field is left as it was. Elsewhere Android sizes the field by the
|
|
541
|
-
* and centres the text in it itself, and keeps it.
|
|
542
|
+
* alone, so on Android such a field is left as it was. Elsewhere Android sizes the field by the
|
|
543
|
+
* line height and centres the text in it itself, and keeps it.
|
|
542
544
|
*
|
|
543
545
|
* On iOS, React Native's field sets `lineHeight` as the paragraph's minimum and maximum line
|
|
544
546
|
* height and, unlike a paragraph (`RCTApplyBaselineOffset`), never offsets the baseline, so the
|
|
@@ -561,10 +563,9 @@ function centreSingleLine(viewName, props, fontScale, onBaseline = false) {
|
|
|
561
563
|
heightBaseline(props, height, onBaseline, fontScale);
|
|
562
564
|
return;
|
|
563
565
|
}
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// Android sizes the field by its line height, and centres the text in it, itself.
|
|
566
|
+
// Android sizes the field by its line height, and centres the text in it, itself. A
|
|
567
|
+
// percentage is a fixed height only where the parent's is definite, known at layout alone:
|
|
568
|
+
// there too the line height is left out on iOS, and kept as the least the field is tall.
|
|
568
569
|
if (viewName !== 'TextInput')
|
|
569
570
|
return;
|
|
570
571
|
const edge = (side, logical) => blockEdge(props, side, logical);
|
|
@@ -847,10 +848,11 @@ const TOUCHED_ALPHA = 0.011;
|
|
|
847
848
|
function stillTouched(props) {
|
|
848
849
|
// One told to take no touch is not to be sent one, seen or not.
|
|
849
850
|
if (props['pointerEvents'] === 'none')
|
|
850
|
-
return;
|
|
851
|
+
return props;
|
|
851
852
|
const opacity = props['opacity'];
|
|
852
853
|
if (typeof opacity === 'number' && opacity < TOUCHED_ALPHA)
|
|
853
854
|
props['opacity'] = TOUCHED_ALPHA;
|
|
855
|
+
return props;
|
|
854
856
|
}
|
|
855
857
|
/**
|
|
856
858
|
* Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
|
|
@@ -865,6 +867,29 @@ function ratioForAutoSize(props) {
|
|
|
865
867
|
delete props['aspectRatio'];
|
|
866
868
|
}
|
|
867
869
|
const OUTLINE_KEYS = ['outlineWidth', 'outlineStyle', 'outlineColor', 'outlineOffset'];
|
|
870
|
+
/** Whether a side of a border has a width and nothing that gives it a colour. */
|
|
871
|
+
function colourless(props, side, flipped) {
|
|
872
|
+
const width = props[side.width] ?? props['borderWidth'];
|
|
873
|
+
if (typeof width !== 'number' || width <= 0)
|
|
874
|
+
return false;
|
|
875
|
+
const given = [side.color, flipped ? side.flipped : side.logical, 'borderColor'];
|
|
876
|
+
return given.every((key) => props[key] == null);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Each side a border has: its width, its colour, and the logical colour React Native takes for
|
|
880
|
+
* it too, read left to right and, `flipped`, right to left.
|
|
881
|
+
*/
|
|
882
|
+
const BORDER_SIDES = [
|
|
883
|
+
['Top', 'BlockStart', 'BlockStart'],
|
|
884
|
+
['Right', 'End', 'Start'],
|
|
885
|
+
['Bottom', 'BlockEnd', 'BlockEnd'],
|
|
886
|
+
['Left', 'Start', 'End'],
|
|
887
|
+
].map(([side, logical, flipped]) => ({
|
|
888
|
+
width: `border${side}Width`,
|
|
889
|
+
color: `border${side}Color`,
|
|
890
|
+
logical: `border${logical}Color`,
|
|
891
|
+
flipped: `border${flipped}Color`,
|
|
892
|
+
}));
|
|
868
893
|
/**
|
|
869
894
|
* Send nothing for an outline of no width, which is what `outline: none` is: it draws nothing,
|
|
870
895
|
* and a view with no outline is the same view. Sent, it is an update that is not one, and on
|
|
@@ -916,6 +941,64 @@ function alignMultiline(viewName, props) {
|
|
|
916
941
|
return;
|
|
917
942
|
props['textAlignVertical'] ??= 'top';
|
|
918
943
|
}
|
|
944
|
+
/** The props of a run of text that change how tall its line is drawn. */
|
|
945
|
+
const SIZES_A_RUN = new Set(['style', 'allowFontScaling', 'maxFontSizeMultiplier']);
|
|
946
|
+
/**
|
|
947
|
+
* Keep a paragraph as tall as one of its lines, as a browser's line box is whatever holds it.
|
|
948
|
+
* Yoga measures an item no taller than the room inside its container, and the text view cuts
|
|
949
|
+
* its letters off there: a 24 point line in a row with 16 points between its paddings. The
|
|
950
|
+
* line is the paragraph's own or a taller one of a run in it, `run`, which is scaled already. With it goes what is
|
|
951
|
+
* around the text and inside the height Yoga gives a view, its padding and border above and
|
|
952
|
+
* below, but for a content box. Only where a line height is a length, and nothing gives the
|
|
953
|
+
* paragraph a height of its own.
|
|
954
|
+
*
|
|
955
|
+
* The line is as the system text size draws it: native scales a line height with the text, so
|
|
956
|
+
* a line held to its points at a smaller size is shorter than its box and sits at the top of
|
|
957
|
+
* it, over what a row centres it beside.
|
|
958
|
+
*/
|
|
959
|
+
function lineTall(props, run, fontScale) {
|
|
960
|
+
const own = props['lineHeight'];
|
|
961
|
+
const line = Math.max(typeof own === 'number' ? own * textScale(props, fontScale) : 0, run);
|
|
962
|
+
if (line <= 0)
|
|
963
|
+
return;
|
|
964
|
+
const unset = (value) => value == null || value === 'auto';
|
|
965
|
+
if (!unset(props['height']) || !unset(props['minHeight']))
|
|
966
|
+
return;
|
|
967
|
+
const around = props['boxSizing'] === 'content-box' ? 0 : aroundALine(props);
|
|
968
|
+
// To a thousandth of a point: a product of two fractions is rarely the number it reads as.
|
|
969
|
+
props['minHeight'] = Math.round((line + around) * 1000) / 1000;
|
|
970
|
+
}
|
|
971
|
+
/** The points of padding and border above and below a view's content, where each is a length. */
|
|
972
|
+
function aroundALine(props) {
|
|
973
|
+
const edge = (...keys) => {
|
|
974
|
+
const found = keys.map((key) => props[key]).find((value) => value != null);
|
|
975
|
+
return typeof found === 'number' ? found : 0;
|
|
976
|
+
};
|
|
977
|
+
return (edge('paddingTop', 'paddingVertical', 'padding') +
|
|
978
|
+
edge('paddingBottom', 'paddingVertical', 'padding') +
|
|
979
|
+
edge('borderTopWidth', 'borderWidth') +
|
|
980
|
+
edge('borderBottomWidth', 'borderWidth'));
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Where a line may break: at a space, after a hyphen, and anywhere in a script written without
|
|
984
|
+
* spaces. A soft hyphen and a zero-width space are places to break that draw nothing.
|
|
985
|
+
*/
|
|
986
|
+
const BREAKS = /[\s\-\u00ad\u200b\u2010\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/;
|
|
987
|
+
/** Whether a paragraph's text, all its runs together, is one word: something, with no break. */
|
|
988
|
+
function unbroken(node) {
|
|
989
|
+
let text = '';
|
|
990
|
+
const collect = (from) => {
|
|
991
|
+
for (const child of from.children) {
|
|
992
|
+
if (child.kind === 'text')
|
|
993
|
+
text += child.text;
|
|
994
|
+
else if (isTextElement(child))
|
|
995
|
+
collect(child);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
collect(node);
|
|
999
|
+
const words = trimEnds(text);
|
|
1000
|
+
return words !== '' && !BREAKS.test(words);
|
|
1001
|
+
}
|
|
919
1002
|
/**
|
|
920
1003
|
* Resolve a paragraph's `text-align` against its direction, as CSS does.
|
|
921
1004
|
*
|
|
@@ -1411,6 +1494,23 @@ const MOVES_BOXES = new Set([
|
|
|
1411
1494
|
...['aspectRatio', 'fontSize', 'lineHeight', 'fontFamily', 'fontWeight', 'letterSpacing'],
|
|
1412
1495
|
...['numberOfLines', 'start', 'end'],
|
|
1413
1496
|
]);
|
|
1497
|
+
/** Whether native is moving a view to a prop's value, by a transition or an animation it laid out. */
|
|
1498
|
+
function laidByNative(node, key) {
|
|
1499
|
+
const eased = node.transitions?.get(key);
|
|
1500
|
+
if (eased?.laid && !eased.done)
|
|
1501
|
+
return true;
|
|
1502
|
+
return node.playing?.laid === true && node.playing.tracks.has(key);
|
|
1503
|
+
}
|
|
1504
|
+
/** Whether an animation plays once through, forwards, with no wait before it. */
|
|
1505
|
+
const playedOnce = (spec) => spec.delay === 0 && spec.duration > 0 && spec.iterations === 1 && !spec.direction;
|
|
1506
|
+
/** Whether a track is two frames on one curve, the first of them where the view is drawn. */
|
|
1507
|
+
function fromWhereDrawn(track, drawn) {
|
|
1508
|
+
const [first, last] = track;
|
|
1509
|
+
if (track.length !== 2 || first.easing || last.value == null)
|
|
1510
|
+
return false;
|
|
1511
|
+
// One that ends where it starts moves nothing: there is nothing for native to lay out.
|
|
1512
|
+
return Object.is(first.value, drawn) && !Object.is(first.value, last.value);
|
|
1513
|
+
}
|
|
1414
1514
|
/** The curves a layout animation has, as the timing function each is. */
|
|
1415
1515
|
const LAYOUT_CURVES = [
|
|
1416
1516
|
['linear', [0, 0, 1, 1]],
|
|
@@ -1555,6 +1655,7 @@ function noteTouches(node, style) {
|
|
|
1555
1655
|
if (value === 'none')
|
|
1556
1656
|
node.propsDirty = true;
|
|
1557
1657
|
}
|
|
1658
|
+
const hasClasses = (node, names) => names.every((name) => node.classes?.has(name));
|
|
1558
1659
|
/** Whether `node` is `ancestor` or somewhere under it. */
|
|
1559
1660
|
function isWithin(node, ancestor) {
|
|
1560
1661
|
for (let at = node; at; at = at.parent)
|
|
@@ -1645,6 +1746,7 @@ class RetainedNode {
|
|
|
1645
1746
|
customProperties = null;
|
|
1646
1747
|
styleCache = null;
|
|
1647
1748
|
styleDirty = true;
|
|
1749
|
+
ownDirty = false;
|
|
1648
1750
|
styleCommitted = null;
|
|
1649
1751
|
fitContainer = undefined;
|
|
1650
1752
|
heightBasis = undefined;
|
|
@@ -1657,6 +1759,8 @@ class RetainedNode {
|
|
|
1657
1759
|
committedUnder = null;
|
|
1658
1760
|
kept = undefined;
|
|
1659
1761
|
remeasure = undefined;
|
|
1762
|
+
oneWord = undefined;
|
|
1763
|
+
raised = undefined;
|
|
1660
1764
|
propsDirty = false;
|
|
1661
1765
|
structureDirty = false;
|
|
1662
1766
|
subtreeDirty = false;
|
|
@@ -1861,6 +1965,8 @@ const TRANSITION_TIMING = {
|
|
|
1861
1965
|
$transitionDelay: 'delay',
|
|
1862
1966
|
$transitionEasing: 'easing',
|
|
1863
1967
|
};
|
|
1968
|
+
/** Listed once, as `ANIMATION_ENTRIES` is. */
|
|
1969
|
+
const TRANSITION_ENTRIES = Object.entries(TRANSITION_TIMING);
|
|
1864
1970
|
/**
|
|
1865
1971
|
* The transition spec, with any timing set by a rule of its own laid over it: `.duration-700`
|
|
1866
1972
|
* beside `.transition`. A rule that writes the timing itself compiles it to `null`, which is how
|
|
@@ -1874,7 +1980,7 @@ function transitionSpec(props) {
|
|
|
1874
1980
|
let spec = props['$transition'];
|
|
1875
1981
|
if (spec !== undefined)
|
|
1876
1982
|
delete props['$transition'];
|
|
1877
|
-
for (const [key, part] of
|
|
1983
|
+
for (const [key, part] of TRANSITION_ENTRIES) {
|
|
1878
1984
|
if (!(key in props))
|
|
1879
1985
|
continue;
|
|
1880
1986
|
const value = props[key];
|
|
@@ -1993,6 +2099,14 @@ const now = () => globalThis.performance?.now?.() ?? Date.now();
|
|
|
1993
2099
|
function sameHandles(a, b) {
|
|
1994
2100
|
return a.length === b.length && a.every((handle, i) => handle === b[i]);
|
|
1995
2101
|
}
|
|
2102
|
+
/** Whether two sets of the same classes hold them in the same order. */
|
|
2103
|
+
function inOrder(a, b) {
|
|
2104
|
+
const other = b.values();
|
|
2105
|
+
for (const name of a)
|
|
2106
|
+
if (other.next().value !== name)
|
|
2107
|
+
return false;
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
1996
2110
|
/**
|
|
1997
2111
|
* Whether a node could be the element a compound is written for, by its classes alone: its own,
|
|
1998
2112
|
* and those of one alternative of each `:is()` on it, which is where Tailwind names a group or
|
|
@@ -2322,12 +2436,16 @@ export class Engine {
|
|
|
2322
2436
|
*/
|
|
2323
2437
|
fontsSettled(families) {
|
|
2324
2438
|
let held = false;
|
|
2439
|
+
const release = (node) => {
|
|
2440
|
+
if (node?.heldFamily === undefined || !families.has(node.heldFamily))
|
|
2441
|
+
return;
|
|
2442
|
+
this.markProps(node, false);
|
|
2443
|
+
held = true;
|
|
2444
|
+
};
|
|
2325
2445
|
const visit = (node) => {
|
|
2326
2446
|
for (const child of node.children) {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
held = true;
|
|
2330
|
-
}
|
|
2447
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
2448
|
+
release(child.kind === 'text' ? child.box : child);
|
|
2331
2449
|
visit(child);
|
|
2332
2450
|
}
|
|
2333
2451
|
};
|
|
@@ -2336,22 +2454,28 @@ export class Engine {
|
|
|
2336
2454
|
this.commit();
|
|
2337
2455
|
}
|
|
2338
2456
|
fontsRegistered(families) {
|
|
2457
|
+
/** Lay `node` out again where it is text naming one of the families, saying whether it was. */
|
|
2458
|
+
const refresh = (node) => {
|
|
2459
|
+
// As it is drawn: a text element placing its text is a view, with the paragraph inside it.
|
|
2460
|
+
const viewName = committedViewName(node);
|
|
2461
|
+
const input = TEXT_INPUTS.has(viewName);
|
|
2462
|
+
const named = (viewName === PARAGRAPH || input) && namesFamily(node, families);
|
|
2463
|
+
if (!node.committed || !named)
|
|
2464
|
+
return false;
|
|
2465
|
+
this.fontRefreshes.set(node, (this.fontRefreshes.get(node) ?? 0) + 1);
|
|
2466
|
+
if (input)
|
|
2467
|
+
this.fontResends.add(node);
|
|
2468
|
+
this.fontsRefreshed = true;
|
|
2469
|
+
this.markProps(node, false);
|
|
2470
|
+
return true;
|
|
2471
|
+
};
|
|
2339
2472
|
const visit = (node) => {
|
|
2340
2473
|
for (const child of node.children) {
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
if (child.committed && (viewName === PARAGRAPH || input) && namesFamily(child, families)) {
|
|
2346
|
-
this.fontRefreshes.set(child, (this.fontRefreshes.get(child) ?? 0) + 1);
|
|
2347
|
-
if (input)
|
|
2348
|
-
this.fontResends.add(child);
|
|
2349
|
-
this.fontsRefreshed = true;
|
|
2350
|
-
this.markProps(child, false);
|
|
2351
|
-
}
|
|
2352
|
-
else {
|
|
2474
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
2475
|
+
if (child.kind === 'text' && child.box)
|
|
2476
|
+
refresh(child.box);
|
|
2477
|
+
if (child.kind === 'element' && !refresh(child))
|
|
2353
2478
|
visit(child);
|
|
2354
|
-
}
|
|
2355
2479
|
}
|
|
2356
2480
|
};
|
|
2357
2481
|
visit(this.root);
|
|
@@ -2495,34 +2619,46 @@ export class Engine {
|
|
|
2495
2619
|
* Mark a node whose classes changed, by what the rules that name those classes can restyle.
|
|
2496
2620
|
*
|
|
2497
2621
|
* A class no selector names changes no style: a library's own marker, of an overlay that is
|
|
2498
|
-
* animating or a control that was touched. One
|
|
2499
|
-
*
|
|
2500
|
-
*
|
|
2501
|
-
*
|
|
2622
|
+
* animating or a control that was touched. One a rule is for the element of, `.on`, has that
|
|
2623
|
+
* element matched again, and one named as what a rule's element is inside, `.animating .close`
|
|
2624
|
+
* or `.close:is(.animating *)`, the elements under the node that such rules are for. What is
|
|
2625
|
+
* under any of those is styled again from the rules it matched, for what it inherits: a `dark`
|
|
2626
|
+
* class on the root finds no rule again for an element no `dark` rule is for. Any other class
|
|
2627
|
+
* has the node and everything under it matched again, as any other change to it does.
|
|
2502
2628
|
*
|
|
2503
2629
|
* A sheet that names a class later restyles what it is for as it arrives, and a node not yet
|
|
2504
2630
|
* committed is styled when it is, with the class as it is then.
|
|
2505
2631
|
*/
|
|
2506
2632
|
markClasses(node, changed) {
|
|
2633
|
+
// An element still being built, in no tree and never styled: nothing is styled by it yet,
|
|
2634
|
+
// and it is styled when it is committed, with the classes it has then. Every element of a
|
|
2635
|
+
// screen is given its classes this way, so what they reach is not asked for each.
|
|
2636
|
+
if (node.parent === null && node.styleCache === null)
|
|
2637
|
+
return this.markProps(node, false);
|
|
2507
2638
|
const reach = this.styles.reachOf(changed);
|
|
2508
2639
|
if (reach === true)
|
|
2509
2640
|
return this.markProps(node);
|
|
2510
2641
|
this.markProps(node, false);
|
|
2511
|
-
if (reach)
|
|
2642
|
+
if (!reach)
|
|
2643
|
+
return;
|
|
2644
|
+
if (reach.own)
|
|
2645
|
+
node.ownDirty = true;
|
|
2646
|
+
if (this.styles.reachesUnder(reach))
|
|
2512
2647
|
this.markUnder(node, reach);
|
|
2513
2648
|
}
|
|
2514
|
-
/**
|
|
2649
|
+
/**
|
|
2650
|
+
* Have matched again each element under a node that one of `subjects`' rules could be for. One
|
|
2651
|
+
* under another is marked too: what is under an element marked this way keeps its rules.
|
|
2652
|
+
*/
|
|
2515
2653
|
markUnder(node, subjects) {
|
|
2516
2654
|
for (const child of node.children) {
|
|
2517
2655
|
if (child.kind !== 'element')
|
|
2518
2656
|
continue;
|
|
2519
2657
|
if (this.styles.isFor(subjects, child)) {
|
|
2520
|
-
|
|
2521
|
-
child.styleDirty = true;
|
|
2658
|
+
child.ownDirty = true;
|
|
2522
2659
|
this.markPath(child);
|
|
2523
2660
|
}
|
|
2524
|
-
|
|
2525
|
-
this.markUnder(child, subjects);
|
|
2661
|
+
this.markUnder(child, subjects);
|
|
2526
2662
|
}
|
|
2527
2663
|
}
|
|
2528
2664
|
/**
|
|
@@ -2546,9 +2682,13 @@ export class Engine {
|
|
|
2546
2682
|
*/
|
|
2547
2683
|
setDefaultStyle(node, style) {
|
|
2548
2684
|
node.defaultStyle = style;
|
|
2549
|
-
|
|
2685
|
+
// Under what the cascade comes to, and read by no selector: nothing is matched again.
|
|
2686
|
+
this.markProps(node, false);
|
|
2550
2687
|
}
|
|
2551
|
-
/**
|
|
2688
|
+
/**
|
|
2689
|
+
* `class="a b"` from a template. Replaces the set rather than adding to it. The same classes in
|
|
2690
|
+
* another order are a change too, to a selector that reads the class attribute, `[class^="a"]`.
|
|
2691
|
+
*/
|
|
2552
2692
|
setClasses(node, value) {
|
|
2553
2693
|
const names = value.split(/\s+/).filter(Boolean);
|
|
2554
2694
|
const before = node.classes;
|
|
@@ -2558,7 +2698,8 @@ export class Engine {
|
|
|
2558
2698
|
for (const name of before ?? [])
|
|
2559
2699
|
if (!node.classes?.has(name))
|
|
2560
2700
|
changed.push(name);
|
|
2561
|
-
|
|
2701
|
+
const reordered = !changed.length && !!before && !!node.classes && !inOrder(before, node.classes);
|
|
2702
|
+
this.markClasses(node, reordered ? node.classes : changed);
|
|
2562
2703
|
}
|
|
2563
2704
|
/** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
|
|
2564
2705
|
createAnchor() {
|
|
@@ -2570,6 +2711,7 @@ export class Engine {
|
|
|
2570
2711
|
return node;
|
|
2571
2712
|
}
|
|
2572
2713
|
appendChild(parent, child) {
|
|
2714
|
+
this.markAdopted(child, parent);
|
|
2573
2715
|
this.detach(child);
|
|
2574
2716
|
child.parent = parent;
|
|
2575
2717
|
parent.children.push(child);
|
|
@@ -2579,6 +2721,7 @@ export class Engine {
|
|
|
2579
2721
|
this.markTextContent(parent, child);
|
|
2580
2722
|
}
|
|
2581
2723
|
insertBefore(parent, child, ref) {
|
|
2724
|
+
this.markAdopted(child, parent);
|
|
2582
2725
|
this.detach(child);
|
|
2583
2726
|
child.parent = parent;
|
|
2584
2727
|
const at = ref ? parent.children.indexOf(ref) : -1;
|
|
@@ -2591,6 +2734,16 @@ export class Engine {
|
|
|
2591
2734
|
this.markStructure(parent, child, at < 0 ? parent.children.length - 1 : at);
|
|
2592
2735
|
this.markTextContent(parent, child);
|
|
2593
2736
|
}
|
|
2737
|
+
/**
|
|
2738
|
+
* A node coming under a parent other than the one it is under, or under any once it has been
|
|
2739
|
+
* taken out: what is over it may be other than it was styled under. The resolver sees that for
|
|
2740
|
+
* itself by the parent's cache, except between two boxes that share one, where it is told here.
|
|
2741
|
+
* Moved within its own parent, a row of a list reordered, it is styled as it was.
|
|
2742
|
+
*/
|
|
2743
|
+
markAdopted(child, parent) {
|
|
2744
|
+
if (child.parent !== parent && this.styles.styledUnderShared(child))
|
|
2745
|
+
child.styleDirty = true;
|
|
2746
|
+
}
|
|
2594
2747
|
removeChild(parent, child) {
|
|
2595
2748
|
const target = parent ?? child.parent;
|
|
2596
2749
|
if (!target)
|
|
@@ -2609,8 +2762,27 @@ export class Engine {
|
|
|
2609
2762
|
* changes it too, by keeping or dropping the space before it.
|
|
2610
2763
|
*/
|
|
2611
2764
|
markTextContent(parent, child) {
|
|
2612
|
-
if (child.kind
|
|
2765
|
+
if (child.kind === 'anchor')
|
|
2766
|
+
return;
|
|
2767
|
+
if (takesTextAsProp(parent))
|
|
2613
2768
|
this.markProps(parent);
|
|
2769
|
+
else if (isTextElement(parent))
|
|
2770
|
+
this.markWords(parent, child.kind === 'element');
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Text came, went or changed in a paragraph: where it has become one word, or stopped being
|
|
2774
|
+
* one, the paragraph is merged again, since that is one line or as many as it takes. Asked
|
|
2775
|
+
* of the paragraph's outermost element, which is the view; any other change leaves it be.
|
|
2776
|
+
*/
|
|
2777
|
+
markWords(within, run = false) {
|
|
2778
|
+
let root = within;
|
|
2779
|
+
while (isTextElement(root.parent))
|
|
2780
|
+
root = root.parent;
|
|
2781
|
+
// A run that came or went can be the tallest line of the paragraph: see `lineTall`.
|
|
2782
|
+
// ponytail: one restyled to another line height is not; mark its paragraph if seen cut.
|
|
2783
|
+
const words = root.oneWord !== undefined && unbroken(root) !== root.oneWord;
|
|
2784
|
+
if (run || words)
|
|
2785
|
+
this.markProps(root, false);
|
|
2614
2786
|
}
|
|
2615
2787
|
/** A subtree that went out of the tree is back in: its hoisted nodes commit again. */
|
|
2616
2788
|
wakeHoists(node) {
|
|
@@ -2704,6 +2876,11 @@ export class Engine {
|
|
|
2704
2876
|
this.stopNative(node, node.playing);
|
|
2705
2877
|
node.playing = undefined;
|
|
2706
2878
|
this.stopScrolled(node);
|
|
2879
|
+
// And what follows it, where it is the scroll view: each starts again if it is put back.
|
|
2880
|
+
for (const follower of this.scrollTimelines.get(node) ?? []) {
|
|
2881
|
+
this.stopScrolled(follower);
|
|
2882
|
+
this.markProps(follower, false);
|
|
2883
|
+
}
|
|
2707
2884
|
this.running.delete(node);
|
|
2708
2885
|
this.playing.delete(node);
|
|
2709
2886
|
this.hoisted.delete(node);
|
|
@@ -2748,6 +2925,19 @@ export class Engine {
|
|
|
2748
2925
|
// field's text on every key, match nothing they did not.
|
|
2749
2926
|
const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
|
|
2750
2927
|
this.markProps(node, read || heirs);
|
|
2928
|
+
if (SIZES_A_RUN.has(key))
|
|
2929
|
+
this.markParagraph(node);
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Have the paragraph a run of text is in merged again: it is as tall as the tallest line of
|
|
2933
|
+
* its runs, by a line height or a text scaling set on one of them. See `lineTall`.
|
|
2934
|
+
*/
|
|
2935
|
+
markParagraph(run) {
|
|
2936
|
+
let paragraph = run;
|
|
2937
|
+
while (paragraph.parent && isTextElement(paragraph.parent))
|
|
2938
|
+
paragraph = paragraph.parent;
|
|
2939
|
+
if (paragraph !== run)
|
|
2940
|
+
this.markProps(paragraph, false);
|
|
2751
2941
|
}
|
|
2752
2942
|
/**
|
|
2753
2943
|
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
@@ -2803,6 +2993,7 @@ export class Engine {
|
|
|
2803
2993
|
*/
|
|
2804
2994
|
styleChanged(node) {
|
|
2805
2995
|
this.markProps(node, this.inlineReachesStyle(node));
|
|
2996
|
+
this.markParagraph(node);
|
|
2806
2997
|
}
|
|
2807
2998
|
/**
|
|
2808
2999
|
* A bound declaration, `[style.color]`, whose value may be a `var()`. One that is, is kept as a
|
|
@@ -2842,7 +3033,10 @@ export class Engine {
|
|
|
2842
3033
|
else {
|
|
2843
3034
|
node.customProperties = { ...current, [name]: token };
|
|
2844
3035
|
}
|
|
2845
|
-
|
|
3036
|
+
// In scope for the node's own rules and for what is under it, and read by no selector: the
|
|
3037
|
+
// node is styled again, and those under it from the rules they matched.
|
|
3038
|
+
this.markProps(node, false);
|
|
3039
|
+
node.ownDirty = true;
|
|
2846
3040
|
}
|
|
2847
3041
|
setText(node, value) {
|
|
2848
3042
|
const filled = (node.text === '') !== (value === '');
|
|
@@ -2895,7 +3089,11 @@ export class Engine {
|
|
|
2895
3089
|
}
|
|
2896
3090
|
/** Start matching ancestors again on a change, once a sheet that uses `:has()` is in play. */
|
|
2897
3091
|
watchHas(sheet) {
|
|
2898
|
-
if (!sheet?.has
|
|
3092
|
+
if (!sheet?.has)
|
|
3093
|
+
return;
|
|
3094
|
+
if (!this.hasReaders.has(sheet))
|
|
3095
|
+
this.hasReaders.set(sheet, hasAbove(sheet));
|
|
3096
|
+
if (this.hasSheets)
|
|
2899
3097
|
return;
|
|
2900
3098
|
this.hasSheets = true;
|
|
2901
3099
|
this.styles.tracksHas = true;
|
|
@@ -2912,15 +3110,53 @@ export class Engine {
|
|
|
2912
3110
|
* which would restyle everything under them. Only sheets that use `:has()` pay.
|
|
2913
3111
|
*/
|
|
2914
3112
|
markBeneath(from) {
|
|
2915
|
-
for (let node = from; node; node = node.parent)
|
|
3113
|
+
for (let node = from; node; node = node.parent) {
|
|
2916
3114
|
node.hasDirty = true;
|
|
3115
|
+
if (this.hasReaders.size)
|
|
3116
|
+
this.changedBeneath.add(node);
|
|
3117
|
+
}
|
|
2917
3118
|
}
|
|
3119
|
+
/**
|
|
3120
|
+
* The boxes rules ask `:has()` of above the node they style, `.card:has(.x) .title`, by the
|
|
3121
|
+
* sheet that asks: a sheet replaced or taken away asks no more.
|
|
3122
|
+
*/
|
|
3123
|
+
hasReaders = new Map();
|
|
3124
|
+
/** The nodes something changed beneath since the last commit, where any sheet has such a rule. */
|
|
3125
|
+
changedBeneath = new Set();
|
|
3126
|
+
/**
|
|
3127
|
+
* Have matched again what is under each box that changed beneath and that such a rule could
|
|
3128
|
+
* be asking: the nodes the rule is for, and no other. Once for a commit, however many changes
|
|
3129
|
+
* there were beneath the box: a table gains its rows one at a time.
|
|
3130
|
+
*/
|
|
3131
|
+
markUnderHas() {
|
|
3132
|
+
if (!this.changedBeneath.size)
|
|
3133
|
+
return;
|
|
3134
|
+
for (const node of this.changedBeneath) {
|
|
3135
|
+
for (const readers of this.hasReaders.values()) {
|
|
3136
|
+
for (const { classes, subjects } of readers) {
|
|
3137
|
+
if (classes.every((name) => node.classes?.has(name)))
|
|
3138
|
+
this.markUnder(node, subjects);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
this.changedBeneath.clear();
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* What `node` matches changed, which the elements after it can read: as many of them as a rule
|
|
3146
|
+
* steps along from one, every one for a `~`, and none where no sheet steps at all.
|
|
3147
|
+
*/
|
|
2918
3148
|
markLaterSiblings(node) {
|
|
2919
|
-
|
|
3149
|
+
let left = this.structuralAfter;
|
|
3150
|
+
const siblings = left > 0 ? node.parent?.children : undefined;
|
|
2920
3151
|
if (!siblings)
|
|
2921
3152
|
return;
|
|
2922
|
-
|
|
2923
|
-
|
|
3153
|
+
// Each matched again, and left as it was with all under it where it matches what it did:
|
|
3154
|
+
// see `markPlace`.
|
|
3155
|
+
for (let i = siblings.indexOf(node) + 1; i < siblings.length && left > 0; i++) {
|
|
3156
|
+
const sibling = siblings[i];
|
|
3157
|
+
this.markPlace(sibling);
|
|
3158
|
+
if (sibling.kind === 'element')
|
|
3159
|
+
left--;
|
|
2924
3160
|
}
|
|
2925
3161
|
}
|
|
2926
3162
|
/** `node`'s child list changed: `moved` came into it at `at`, or went out of it from there. */
|
|
@@ -2995,9 +3231,16 @@ export class Engine {
|
|
|
2995
3231
|
* which every such compound it could be asks for: a compound with none could be any element.
|
|
2996
3232
|
*/
|
|
2997
3233
|
placeReadFromUnder(node) {
|
|
2998
|
-
for (const compound of this.placeElsewhere) {
|
|
2999
|
-
if (
|
|
3234
|
+
for (const { compound, above, child } of this.placeElsewhere) {
|
|
3235
|
+
if (!hasClasses(node, compound.classes))
|
|
3236
|
+
continue;
|
|
3237
|
+
// And by what the rule has it in, which a compound with no class is told from.
|
|
3238
|
+
if (!above?.length)
|
|
3000
3239
|
return true;
|
|
3240
|
+
for (let up = node.parent; up; up = child ? null : up.parent) {
|
|
3241
|
+
if (hasClasses(up, above))
|
|
3242
|
+
return true;
|
|
3243
|
+
}
|
|
3001
3244
|
}
|
|
3002
3245
|
return false;
|
|
3003
3246
|
}
|
|
@@ -3048,6 +3291,7 @@ export class Engine {
|
|
|
3048
3291
|
// completeRoot that changes nothing: during a fling that is one wasted native commit per
|
|
3049
3292
|
// frame, since any listener firing marks the view for refresh.
|
|
3050
3293
|
this.settleCreated();
|
|
3294
|
+
this.markUnderHas();
|
|
3051
3295
|
if (!this.root.structureDirty && !this.root.subtreeDirty)
|
|
3052
3296
|
return false;
|
|
3053
3297
|
const started = now();
|
|
@@ -3297,8 +3541,11 @@ export class Engine {
|
|
|
3297
3541
|
flattenStyle(node.props[STYLE_OVERRIDE], style);
|
|
3298
3542
|
this.fontFaces.apply(style);
|
|
3299
3543
|
holdLoadingFamily(node, style);
|
|
3300
|
-
if (viewName === PARAGRAPH)
|
|
3544
|
+
if (viewName === PARAGRAPH) {
|
|
3301
3545
|
alignText(style, this.directionOf(node, style));
|
|
3546
|
+
this.wholeWords(node, style);
|
|
3547
|
+
lineTall(style, this.tallestRun(node, style, 0), this.fontScale);
|
|
3548
|
+
}
|
|
3302
3549
|
if (this.fontsRefreshed)
|
|
3303
3550
|
this.capForFonts(node, style);
|
|
3304
3551
|
alignMultiline(viewName, style);
|
|
@@ -3314,6 +3561,7 @@ export class Engine {
|
|
|
3314
3561
|
// After the basis, which is a size given where it is committed as one.
|
|
3315
3562
|
ratioForAutoSize(merged);
|
|
3316
3563
|
noOutline(merged);
|
|
3564
|
+
this.borderInText(node, merged);
|
|
3317
3565
|
// Last, on what is committed: an override or an animation can hide a box, or place it.
|
|
3318
3566
|
hiddenOutOfFlow(merged);
|
|
3319
3567
|
delete merged['touchAction'];
|
|
@@ -3321,8 +3569,103 @@ export class Engine {
|
|
|
3321
3569
|
stillTouched(merged);
|
|
3322
3570
|
this.heldFor(node, merged);
|
|
3323
3571
|
this.movePaint(node, merged);
|
|
3572
|
+
if (node.raised !== undefined)
|
|
3573
|
+
merged['zIndex'] ??= node.raised;
|
|
3324
3574
|
return merged;
|
|
3325
3575
|
}
|
|
3576
|
+
/**
|
|
3577
|
+
* The z-index a view is drawn at for the boxes in it: the highest of theirs. A browser draws a
|
|
3578
|
+
* box with a z-index over what follows the boxes it is in, and a native view is ordered among
|
|
3579
|
+
* the views beside it and no further, so each view the box is in is raised as far. Read from
|
|
3580
|
+
* what its children committed, which for a raised child is what raised it. A child with a
|
|
3581
|
+
* z-index of its own answers with that and nothing from inside it, as the stacking context it
|
|
3582
|
+
* is. Only a plain view is raised: a scroll view clips what is in it, and a screen is ordered
|
|
3583
|
+
* by its stack.
|
|
3584
|
+
*
|
|
3585
|
+
* ponytail: a z-index is the one thing read as making a stacking context. Opacity and a
|
|
3586
|
+
* transform make one in a browser too; read them here if a box is ever raised past one.
|
|
3587
|
+
*/
|
|
3588
|
+
raisedBy(node, viewName) {
|
|
3589
|
+
if (viewName !== DEFAULT_VIEW)
|
|
3590
|
+
return undefined;
|
|
3591
|
+
let over = 0;
|
|
3592
|
+
for (const child of node.children) {
|
|
3593
|
+
const z = child.committed?.props['zIndex'];
|
|
3594
|
+
if (typeof z !== 'number' || z <= over)
|
|
3595
|
+
continue;
|
|
3596
|
+
if (!this.committedElsewhere(child) && !this.withheld(child))
|
|
3597
|
+
over = z;
|
|
3598
|
+
}
|
|
3599
|
+
return over || undefined;
|
|
3600
|
+
}
|
|
3601
|
+
/** Read how far a view is raised, once its children are committed: its props are, if it moved. */
|
|
3602
|
+
raise(node, viewName) {
|
|
3603
|
+
const raised = this.raisedBy(node, viewName);
|
|
3604
|
+
if (raised === node.raised)
|
|
3605
|
+
return;
|
|
3606
|
+
node.raised = raised;
|
|
3607
|
+
node.propsDirty = true;
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Draw a border that has a width and no colour in the colour of the text, as a browser does:
|
|
3611
|
+
* `border-color` starts as `currentColor`, and native starts it as black. Each side with a
|
|
3612
|
+
* width, where nothing gave that side a colour. Left to native where there is no text colour
|
|
3613
|
+
* to take, which is black in a browser too.
|
|
3614
|
+
*/
|
|
3615
|
+
borderInText(node, props) {
|
|
3616
|
+
let text;
|
|
3617
|
+
// The side a logical colour is for is the other one where text is read right to left.
|
|
3618
|
+
const flipped = (props['borderStartColor'] != null || props['borderEndColor'] != null) &&
|
|
3619
|
+
this.styles.readsRightToLeft(props);
|
|
3620
|
+
for (const side of BORDER_SIDES) {
|
|
3621
|
+
if (!colourless(props, side, flipped))
|
|
3622
|
+
continue;
|
|
3623
|
+
text ??= props['color'] ?? this.styles.resolve(node, this.styleEpoch).inherited['color'];
|
|
3624
|
+
if (text == null)
|
|
3625
|
+
return;
|
|
3626
|
+
props[side.color] = text;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* The tallest line, as the system text size draws it, of the runs of text in a paragraph:
|
|
3631
|
+
* none is 0. A run scales as what it is in does, `within`, but for what it says of that
|
|
3632
|
+
* itself: one that does not scale, or stops at a size, beside text that does.
|
|
3633
|
+
*/
|
|
3634
|
+
tallestRun(node, within, tallest) {
|
|
3635
|
+
for (const child of node.children) {
|
|
3636
|
+
if (!isTextElement(child))
|
|
3637
|
+
continue;
|
|
3638
|
+
const scaling = {
|
|
3639
|
+
allowFontScaling: child.props['allowFontScaling'] ?? within['allowFontScaling'],
|
|
3640
|
+
maxFontSizeMultiplier: child.props['maxFontSizeMultiplier'] ?? within['maxFontSizeMultiplier'],
|
|
3641
|
+
};
|
|
3642
|
+
const line = inlineOf(child)['lineHeight'] ??
|
|
3643
|
+
this.styles.resolve(child, this.styleEpoch).style['lineHeight'];
|
|
3644
|
+
const drawn = typeof line === 'number' ? line * textScale(scaling, this.fontScale) : 0;
|
|
3645
|
+
tallest = this.tallestRun(child, scaling, Math.max(tallest, drawn));
|
|
3646
|
+
}
|
|
3647
|
+
return tallest;
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Keep a paragraph whose text has nowhere to wrap on one line, as a browser does: a word is
|
|
3651
|
+
* not broken inside, and what does not fit is past the edge. Native breaks a word wider than
|
|
3652
|
+
* its line between any two letters. Cut at the edge, where no rule says how it ends.
|
|
3653
|
+
*/
|
|
3654
|
+
wholeWords(node, props) {
|
|
3655
|
+
// Remembered, for its text changing to be told from its text changing what this answers.
|
|
3656
|
+
node.oneWord = props['numberOfLines'] == null ? unbroken(node) : undefined;
|
|
3657
|
+
if (!node.oneWord)
|
|
3658
|
+
return;
|
|
3659
|
+
props['numberOfLines'] = 1;
|
|
3660
|
+
const ended = [
|
|
3661
|
+
this.styles.resolve(node, this.styleEpoch).style,
|
|
3662
|
+
node.props,
|
|
3663
|
+
inlineOf(node),
|
|
3664
|
+
flattenStyle(node.props[STYLE_OVERRIDE], {}),
|
|
3665
|
+
];
|
|
3666
|
+
if (ended.every((from) => from['ellipsizeMode'] == null))
|
|
3667
|
+
props['ellipsizeMode'] = 'clip';
|
|
3668
|
+
}
|
|
3326
3669
|
/**
|
|
3327
3670
|
* Leave a node's background out of what it commits when a child paints it, and give that child
|
|
3328
3671
|
* its parent's: the cascade's, and over it the parent's inline style.
|
|
@@ -3439,6 +3782,8 @@ export class Engine {
|
|
|
3439
3782
|
sheetReplaced(sheet, next) {
|
|
3440
3783
|
if (sheet === next && this.knownSheets.has(sheet))
|
|
3441
3784
|
return;
|
|
3785
|
+
this.hasReaders.delete(sheet);
|
|
3786
|
+
this.watchHas(next);
|
|
3442
3787
|
this.unwatchActive(sheet);
|
|
3443
3788
|
this.watchActive(next);
|
|
3444
3789
|
const at = this.sheetOrder.indexOf(sheet);
|
|
@@ -3538,7 +3883,8 @@ export class Engine {
|
|
|
3538
3883
|
}
|
|
3539
3884
|
return props;
|
|
3540
3885
|
}
|
|
3541
|
-
const
|
|
3886
|
+
const written = this.keyframesOf(node, spec.name);
|
|
3887
|
+
const frames = written && this.framesFor(node, written);
|
|
3542
3888
|
if (!frames) {
|
|
3543
3889
|
// Keyframes a hot swap deleted, from under an animation that was playing them, or ones a
|
|
3544
3890
|
// sheet later in this commit has: see `settleKeyframes`.
|
|
@@ -3554,6 +3900,49 @@ export class Engine {
|
|
|
3554
3900
|
this.startPlaying(node, spec, frames, props);
|
|
3555
3901
|
return Object.assign(props, playedOver(node));
|
|
3556
3902
|
}
|
|
3903
|
+
/**
|
|
3904
|
+
* The keyframes a node's animation names: those of its own component's sheet where it has
|
|
3905
|
+
* some by that name, as Angular scopes a component's keyframes in a browser, and otherwise
|
|
3906
|
+
* whichever sheet has them, since any other is global.
|
|
3907
|
+
*
|
|
3908
|
+
* ponytail: a host is asked of the sheet it is written in before its own component's, so
|
|
3909
|
+
* a `:host` animation is played by its parent's frames where both have the name.
|
|
3910
|
+
*/
|
|
3911
|
+
keyframesOf(node, name) {
|
|
3912
|
+
const own = node.sheet?.keyframes?.[name] ?? node.hostSheet?.keyframes?.[name];
|
|
3913
|
+
return own ?? this.keyframes.get(name);
|
|
3914
|
+
}
|
|
3915
|
+
/** Each node's frames as they settle on it, kept for as long as they settle the same. */
|
|
3916
|
+
settledFrames = new WeakMap();
|
|
3917
|
+
/**
|
|
3918
|
+
* An animation's frames for the node playing it: those with a value that waits for the node,
|
|
3919
|
+
* a `var()` it sets, settled against it. The same frames are answered while they settle the
|
|
3920
|
+
* same, and others once a variable changes, which is what an animation under way is played
|
|
3921
|
+
* on from: see `reframe`.
|
|
3922
|
+
*/
|
|
3923
|
+
framesFor(node, frames) {
|
|
3924
|
+
if (!frames.some((frame) => frame.deferred))
|
|
3925
|
+
return frames;
|
|
3926
|
+
const kept = this.settledFrames.get(node);
|
|
3927
|
+
// Styled as it was, it settles as it did: a commit a frame asks, and most change nothing.
|
|
3928
|
+
const styled = node.styleCache;
|
|
3929
|
+
if (kept?.of === frames && kept.styled === styled)
|
|
3930
|
+
return kept.frames;
|
|
3931
|
+
const settled = frames.map((frame) => frame.deferred && this.styles.settleFor(node, frame.deferred, this.styleEpoch));
|
|
3932
|
+
const key = JSON.stringify(settled);
|
|
3933
|
+
if (kept?.of === frames && kept.key === key) {
|
|
3934
|
+
kept.styled = styled;
|
|
3935
|
+
return kept.frames;
|
|
3936
|
+
}
|
|
3937
|
+
const made = frames.map((frame, at) => {
|
|
3938
|
+
const values = Object.entries(settled[at] ?? {}).filter(([, value]) => value !== undefined);
|
|
3939
|
+
return values.length
|
|
3940
|
+
? { ...frame, declarations: { ...frame.declarations, ...Object.fromEntries(values) } }
|
|
3941
|
+
: frame;
|
|
3942
|
+
});
|
|
3943
|
+
this.settledFrames.set(node, { of: frames, styled, key, frames: made });
|
|
3944
|
+
return made;
|
|
3945
|
+
}
|
|
3557
3946
|
/**
|
|
3558
3947
|
* A clock animation the scroll plays instead: the same animation where only the timeline moved,
|
|
3559
3948
|
* and one cancelled where the name changed.
|
|
@@ -3749,8 +4138,8 @@ export class Engine {
|
|
|
3749
4138
|
node.playing = started;
|
|
3750
4139
|
if (spec.paused)
|
|
3751
4140
|
started.pausedAt = this.now();
|
|
3752
|
-
else
|
|
3753
|
-
this.
|
|
4141
|
+
else
|
|
4142
|
+
this.playSomewhere(node, started, props);
|
|
3754
4143
|
this.emitTransition(node, 'topAnimationstart', spec.name);
|
|
3755
4144
|
return;
|
|
3756
4145
|
}
|
|
@@ -3764,6 +4153,13 @@ export class Engine {
|
|
|
3764
4153
|
}
|
|
3765
4154
|
this.playOrPause(node, node.playing, spec);
|
|
3766
4155
|
}
|
|
4156
|
+
/** Play an animation that has just started: by native's layout, by native's driver, or here. */
|
|
4157
|
+
playSomewhere(node, started, props) {
|
|
4158
|
+
if (this.layPlayed(node, started))
|
|
4159
|
+
this.laidPlayed.push({ node, running: started });
|
|
4160
|
+
else if (!this.playNatively(node, started, props))
|
|
4161
|
+
this.playing.add(node);
|
|
4162
|
+
}
|
|
3767
4163
|
/**
|
|
3768
4164
|
* The colour `node` inherits, for frames that set `color: currentColor`, which is that colour
|
|
3769
4165
|
* wherever the animation has got to, as in CSS. Undefined for frames that do not.
|
|
@@ -3781,7 +4177,8 @@ export class Engine {
|
|
|
3781
4177
|
*/
|
|
3782
4178
|
reframe(node, current, frames, props, inherited) {
|
|
3783
4179
|
const tracks = tracksOf(frames, props, inherited);
|
|
3784
|
-
|
|
4180
|
+
// One native was laying out goes on from JavaScript, by the clock it started on.
|
|
4181
|
+
const reframed = { ...current, tracks, inherited, laid: undefined };
|
|
3785
4182
|
const { values, finished } = sample(reframed, current.pausedAt ?? this.now());
|
|
3786
4183
|
const holds = current.spec.fill === 'forwards' || current.spec.fill === 'both';
|
|
3787
4184
|
reframed.values = finished && !holds ? {} : values;
|
|
@@ -3814,7 +4211,7 @@ export class Engine {
|
|
|
3814
4211
|
/** Every node playing a scroll-driven animation, by the scroll view that plays it. */
|
|
3815
4212
|
scrollTimelines = new Map();
|
|
3816
4213
|
/** How far each such scroll view scrolls, along each axis, once one of its events has said. */
|
|
3817
|
-
scrollExtents = new
|
|
4214
|
+
scrollExtents = new WeakMap();
|
|
3818
4215
|
/**
|
|
3819
4216
|
* An animation played by the nearest scroll view's offset: `animation-timeline: scroll()`.
|
|
3820
4217
|
*
|
|
@@ -3900,8 +4297,13 @@ export class Engine {
|
|
|
3900
4297
|
return;
|
|
3901
4298
|
node.scrolled = undefined;
|
|
3902
4299
|
scrolled.drive?.stop();
|
|
3903
|
-
|
|
3904
|
-
|
|
4300
|
+
const playing = scrolled.source && this.scrollTimelines.get(scrolled.source);
|
|
4301
|
+
if (!playing)
|
|
4302
|
+
return;
|
|
4303
|
+
playing.delete(node);
|
|
4304
|
+
// The last to follow this scroll view: a key left here keeps it, and all it is linked to.
|
|
4305
|
+
if (!playing.size)
|
|
4306
|
+
this.scrollTimelines.delete(scrolled.source);
|
|
3905
4307
|
}
|
|
3906
4308
|
/**
|
|
3907
4309
|
* A scroll view has said how far it scrolls. Every animation on it with a range that depends on
|
|
@@ -3975,7 +4377,8 @@ export class Engine {
|
|
|
3975
4377
|
}
|
|
3976
4378
|
/** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
|
|
3977
4379
|
unseenTransitions(node) {
|
|
3978
|
-
|
|
4380
|
+
// One with no commit it was born in has been seen, whatever was committed since.
|
|
4381
|
+
if (node.bornIn === undefined || node.bornIn < this.unseen)
|
|
3979
4382
|
return;
|
|
3980
4383
|
this.stopEased(node);
|
|
3981
4384
|
node.transitions = undefined;
|
|
@@ -4037,36 +4440,101 @@ export class Engine {
|
|
|
4037
4440
|
if (eased.laid && !eased.done)
|
|
4038
4441
|
props[key] = eased.to;
|
|
4039
4442
|
}
|
|
4443
|
+
/** The animations this commit lays out at their last frame, for native to move the views to. */
|
|
4444
|
+
laidPlayed = [];
|
|
4040
4445
|
/**
|
|
4041
|
-
*
|
|
4042
|
-
*
|
|
4043
|
-
*
|
|
4446
|
+
* Whether an animation that has just started is one native can play by laying the view out
|
|
4447
|
+
* at its last frame: two frames of sizes or places, played once from where the view is drawn
|
|
4448
|
+
* now, which is where native moves it from. It is then committed at that frame; see `layOut`.
|
|
4449
|
+
*/
|
|
4450
|
+
layPlayed(node, running) {
|
|
4451
|
+
const drawn = node.committed?.props;
|
|
4452
|
+
const { spec, tracks } = running;
|
|
4453
|
+
if (!this.fabric.configureNextLayoutAnimation || !drawn || !tracks.size)
|
|
4454
|
+
return false;
|
|
4455
|
+
if (!playedOnce(spec))
|
|
4456
|
+
return false;
|
|
4457
|
+
for (const [key, track] of tracks) {
|
|
4458
|
+
if (!LAID_OUT.has(key) || !fromWhereDrawn(track, drawn[key]))
|
|
4459
|
+
return false;
|
|
4460
|
+
}
|
|
4461
|
+
running.laid = true;
|
|
4462
|
+
running.values = Object.fromEntries([...tracks].map(([key, track]) => [key, track[1].value]));
|
|
4463
|
+
return true;
|
|
4464
|
+
}
|
|
4465
|
+
/** Go on from JavaScript with an animation native was to lay out, by the clock it started on. */
|
|
4466
|
+
playLaidHere(node, running) {
|
|
4467
|
+
running.laid = undefined;
|
|
4468
|
+
running.values = sample(running, this.now()).values;
|
|
4469
|
+
this.playing.add(node);
|
|
4470
|
+
}
|
|
4471
|
+
/** An animation native laid out has had its time: it is at its last frame, and says so. */
|
|
4472
|
+
endLaidPlayed(node, running) {
|
|
4473
|
+
if (!running.laid || running.done || node.playing !== running)
|
|
4474
|
+
return;
|
|
4475
|
+
Object.assign(running, { done: true, laid: undefined });
|
|
4476
|
+
if (!this.keeps(node, running)) {
|
|
4477
|
+
running.values = {};
|
|
4478
|
+
this.markProps(node, false);
|
|
4479
|
+
}
|
|
4480
|
+
this.emitTransition(node, 'topAnimationend', running.spec.name);
|
|
4481
|
+
this.flushTransitionEvents();
|
|
4482
|
+
if (this.pending)
|
|
4483
|
+
this.commit();
|
|
4484
|
+
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Before a commit is handed over: ask native to move what it lays out, and take back to
|
|
4487
|
+
* ease from JavaScript whatever it cannot: all of it where the commit moves anything else,
|
|
4488
|
+
* and otherwise what would not take as long as the longest. Answers whether the tree has to
|
|
4489
|
+
* be reconciled again.
|
|
4044
4490
|
*/
|
|
4045
4491
|
layOut() {
|
|
4046
4492
|
const laying = this.laying;
|
|
4493
|
+
const played = this.laidPlayed.filter(({ node, running }) => node.playing === running);
|
|
4047
4494
|
const others = this.movesOthers;
|
|
4048
4495
|
this.laying = [];
|
|
4496
|
+
this.laidPlayed = [];
|
|
4049
4497
|
this.movesOthers = false;
|
|
4050
|
-
if (!laying.length)
|
|
4498
|
+
if (!laying.length && !played.length)
|
|
4051
4499
|
return false;
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4500
|
+
// Native takes one length of time for a commit: the longest asked of it, and whatever
|
|
4501
|
+
// would take another is eased from JavaScript. None of it, where anything else moves.
|
|
4502
|
+
const longest = Math.max(...laying.map(({ eased }) => eased.duration), ...played.map(({ running }) => running.spec.duration));
|
|
4503
|
+
const kept = (duration) => !others && duration === longest;
|
|
4504
|
+
const back = [];
|
|
4505
|
+
for (const { node, eased } of laying) {
|
|
4506
|
+
if (kept(eased.duration))
|
|
4507
|
+
continue;
|
|
4508
|
+
eased.laid = undefined;
|
|
4509
|
+
this.running.add(node);
|
|
4510
|
+
back.push(node);
|
|
4511
|
+
}
|
|
4512
|
+
for (const { node, running } of played) {
|
|
4513
|
+
if (kept(running.spec.duration))
|
|
4514
|
+
continue;
|
|
4515
|
+
this.playLaidHere(node, running);
|
|
4516
|
+
back.push(node);
|
|
4062
4517
|
}
|
|
4063
|
-
const
|
|
4518
|
+
for (const node of back)
|
|
4519
|
+
this.markProps(node, false);
|
|
4520
|
+
if (!others)
|
|
4521
|
+
this.askLayout(laying, played, longest);
|
|
4522
|
+
return back.length > 0;
|
|
4523
|
+
}
|
|
4524
|
+
/** Ask native to move what the commit lays out over `duration`, and end each when it is up. */
|
|
4525
|
+
askLayout(laying, played, duration) {
|
|
4526
|
+
const easing = laying.find(({ eased }) => eased.laid)?.eased.easing ??
|
|
4527
|
+
played.find(({ running }) => running.laid).running.spec.easing;
|
|
4064
4528
|
const nothing = () => undefined;
|
|
4065
4529
|
this.fabric.configureNextLayoutAnimation({ duration, update: { type: layoutCurve(easing) } }, nothing, nothing);
|
|
4066
4530
|
for (const { node, key, eased } of laying) {
|
|
4067
|
-
|
|
4531
|
+
if (eased.laid)
|
|
4532
|
+
setTimeout(() => this.endLaid(node, key, eased), duration);
|
|
4533
|
+
}
|
|
4534
|
+
for (const { node, running } of played) {
|
|
4535
|
+
if (running.laid)
|
|
4536
|
+
setTimeout(() => this.endLaidPlayed(node, running), duration);
|
|
4068
4537
|
}
|
|
4069
|
-
return false;
|
|
4070
4538
|
}
|
|
4071
4539
|
/**
|
|
4072
4540
|
* Note a commit that resizes or moves a box in no transition native lays out: a prop of one
|
|
@@ -4078,8 +4546,7 @@ export class Engine {
|
|
|
4078
4546
|
if (resized)
|
|
4079
4547
|
this.movesOthers = true;
|
|
4080
4548
|
for (const key in payload) {
|
|
4081
|
-
|
|
4082
|
-
if (eased?.laid && !eased.done)
|
|
4549
|
+
if (laidByNative(node, key))
|
|
4083
4550
|
continue;
|
|
4084
4551
|
// Text as long as it was is taken to be as wide, as a grid takes it: see `shapeOf`.
|
|
4085
4552
|
const longer = key === 'text' && String(payload[key]).length !== String(words).length;
|
|
@@ -4131,13 +4598,16 @@ export class Engine {
|
|
|
4131
4598
|
}
|
|
4132
4599
|
playEased(node, state, keys, props) {
|
|
4133
4600
|
const first = state.get(keys[0]);
|
|
4601
|
+
// Where the view is committed: one that takes a touch stops short of no opacity.
|
|
4602
|
+
const touched = this.responders.has(node);
|
|
4603
|
+
const seen = (key, value) => touched && key === 'opacity' ? stillTouched({ ...props, opacity: value })['opacity'] : value;
|
|
4134
4604
|
const tracks = new Map(keys.map((key) => {
|
|
4135
4605
|
const { current, to, easing } = state.get(key);
|
|
4136
4606
|
return [
|
|
4137
4607
|
key,
|
|
4138
4608
|
[
|
|
4139
|
-
{ offset: 0, value: current, easing },
|
|
4140
|
-
{ offset: 1, value: to },
|
|
4609
|
+
{ offset: 0, value: seen(key, current), easing },
|
|
4610
|
+
{ offset: 1, value: seen(key, to) },
|
|
4141
4611
|
],
|
|
4142
4612
|
];
|
|
4143
4613
|
}));
|
|
@@ -4491,6 +4961,7 @@ export class Engine {
|
|
|
4491
4961
|
this.noteBaselineFields(node);
|
|
4492
4962
|
this.freshBaselines(node);
|
|
4493
4963
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
4964
|
+
this.raise(node, viewName);
|
|
4494
4965
|
noteTouches(node, style);
|
|
4495
4966
|
const previous = node.committed;
|
|
4496
4967
|
this.notePresented(node, viewName);
|
|
@@ -4601,8 +5072,12 @@ export class Engine {
|
|
|
4601
5072
|
}
|
|
4602
5073
|
box.parent = parent;
|
|
4603
5074
|
// The text is not under the box in the tree, so a change to it never marked the box.
|
|
4604
|
-
if (!this.isClean(text))
|
|
5075
|
+
if (!this.isClean(text)) {
|
|
4605
5076
|
box.subtreeDirty = true;
|
|
5077
|
+
// Nor had it merged again where the text became one word, or stopped being one.
|
|
5078
|
+
if (box.oneWord !== undefined && unbroken(box) !== box.oneWord)
|
|
5079
|
+
box.propsDirty = true;
|
|
5080
|
+
}
|
|
4606
5081
|
return box;
|
|
4607
5082
|
}
|
|
4608
5083
|
/** Remember an iOS modal host committed as showing, so hiding it waits for `topDismiss`. */
|
|
@@ -4986,6 +5461,11 @@ export class Engine {
|
|
|
4986
5461
|
setResponder(node, handlers) {
|
|
4987
5462
|
this.registerEventHandler();
|
|
4988
5463
|
this.responders.set(node, handlers);
|
|
5464
|
+
// A fade native is playing was given to it as a view that takes no touch fades: to
|
|
5465
|
+
// nothing. From here it is eased to where the view is committed.
|
|
5466
|
+
const fade = node.transitions?.get('opacity')?.native;
|
|
5467
|
+
if (fade)
|
|
5468
|
+
this.backToScriptEased(node, fade, this.now());
|
|
4989
5469
|
this.keepNative(node, true);
|
|
4990
5470
|
// What it is committed with can hang on whether it takes a touch: see `stillTouched`.
|
|
4991
5471
|
this.markProps(node);
|
|
@@ -5213,12 +5693,14 @@ export class Engine {
|
|
|
5213
5693
|
this.currentResponder = null;
|
|
5214
5694
|
this.tellNative(node, false);
|
|
5215
5695
|
const handlers = this.responders.get(node);
|
|
5696
|
+
// Before the handler is told: what it does is for the render pass after it to commit
|
|
5697
|
+
// whole, and not to be drawn part made with the view no longer pressed.
|
|
5698
|
+
if (changed)
|
|
5699
|
+
this.commit();
|
|
5216
5700
|
if (terminated)
|
|
5217
5701
|
handlers?.onResponderTerminate?.(event);
|
|
5218
5702
|
else
|
|
5219
5703
|
handlers?.onResponderRelease?.(event);
|
|
5220
|
-
if (changed)
|
|
5221
|
-
this.commit();
|
|
5222
5704
|
}
|
|
5223
5705
|
tellNative(node, isResponder) {
|
|
5224
5706
|
const handle = node.committed?.handle;
|