@ng-native/fabric 0.4.0 → 0.5.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/color-mix.js +1 -16
- package/dist/color-mix.js.map +1 -1
- package/dist/css.d.ts +96 -1
- package/dist/css.js +332 -4
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +105 -1
- package/dist/engine.js +489 -22
- package/dist/engine.js.map +1 -1
- package/dist/inline-token.js +3 -4
- package/dist/inline-token.js.map +1 -1
- package/dist/native-drive.d.ts +21 -0
- package/dist/native-drive.js +60 -0
- package/dist/native-drive.js.map +1 -1
- package/dist/premultiplied-stops.d.ts +7 -0
- package/dist/premultiplied-stops.js +131 -0
- package/dist/premultiplied-stops.js.map +1 -0
- package/dist/scroll-animation.d.ts +11 -0
- package/dist/scroll-animation.js +29 -0
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +15 -0
- package/dist/transition.js +42 -1
- 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, inlineInherited, setsInherited, StyleResolver, } from "./css.js";
|
|
10
|
+
import { boundDeclaration, inlineInherited, setsInherited, StyleResolver, usesActive, } from "./css.js";
|
|
11
11
|
import { applyAria } from "./aria-props.js";
|
|
12
12
|
import { STYLED_ELEMENTS } from "./element-styles.js";
|
|
13
13
|
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
|
|
@@ -15,8 +15,9 @@ import { isFormsInput } from "./forms-inputs.js";
|
|
|
15
15
|
import { transformList } from "./inline-transform.js";
|
|
16
16
|
import { tokenFromValue } from "./inline-token.js";
|
|
17
17
|
import { NativeScrollDriver, scrollFeed, } from "./native-drive.js";
|
|
18
|
-
import { firstFrame, rangeOf, scrollChannels } from "./scroll-animation.js";
|
|
18
|
+
import { clockChannels, firstFrame, rangeOf, scrollChannels } from "./scroll-animation.js";
|
|
19
19
|
import { FontFaces } from "./font-faces.js";
|
|
20
|
+
import { premultipliedStops } from "./premultiplied-stops.js";
|
|
20
21
|
/**
|
|
21
22
|
* The animation a node's style asks for, with a play state from a rule of its own applied. Both
|
|
22
23
|
* are instructions for this engine and nothing native has heard of them, so they come out of the
|
|
@@ -459,6 +460,124 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
459
460
|
: box.reduce((sum, part) => sum + part, 0);
|
|
460
461
|
props['minHeight'] = Math.max(own, Math.min(content, max));
|
|
461
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* What a container's own style says for `key`, in the order `mergeProps` applies them: a
|
|
465
|
+
* component's override, inline, a prop, its stylesheet, its default.
|
|
466
|
+
*/
|
|
467
|
+
function ownLayout(node, key) {
|
|
468
|
+
return (flattenStyle(node.props[STYLE_OVERRIDE], {})[key] ??
|
|
469
|
+
flattenStyle(node.props['style'], {})[key] ??
|
|
470
|
+
node.props[key] ??
|
|
471
|
+
node.styleCache?.style[key] ??
|
|
472
|
+
node.defaultStyle?.[key]);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* A container's main axis and how it aligns across it, as `row` or `column` and a keyword. A
|
|
476
|
+
* `display: contents` node is no container: its children are laid out in the one above it.
|
|
477
|
+
*/
|
|
478
|
+
function containerOf(node) {
|
|
479
|
+
if (ownLayout(node, 'display') === 'contents')
|
|
480
|
+
return 'contents';
|
|
481
|
+
const direction = String(ownLayout(node, 'flexDirection') ?? 'column');
|
|
482
|
+
return `${direction.startsWith('row') ? 'row' : 'column'} ${ownLayout(node, 'alignItems') ?? 'stretch'}`;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The node a box is laid out in: its parent, or the one above every `display: contents` ancestor.
|
|
486
|
+
* Each of those is told a `fit-content` child passed through, so that when it stops being
|
|
487
|
+
* `contents`, and is the container, the child is worked out again.
|
|
488
|
+
*/
|
|
489
|
+
function layoutParent(node) {
|
|
490
|
+
let parent = node.parent;
|
|
491
|
+
while (parent && containerOf(parent) === 'contents') {
|
|
492
|
+
parent.fitContainer = 'contents';
|
|
493
|
+
parent = parent.parent;
|
|
494
|
+
}
|
|
495
|
+
return parent;
|
|
496
|
+
}
|
|
497
|
+
const STRETCHES = new Set([undefined, null, 'auto', 'stretch']);
|
|
498
|
+
/**
|
|
499
|
+
* `width: fit-content` and `height: fit-content`, which Yoga has no size for.
|
|
500
|
+
*
|
|
501
|
+
* Along its container's main axis a box is as big as its content already, so the size is
|
|
502
|
+
* dropped. Across it a box stretches, where a browser keeps it at its content's size and at the
|
|
503
|
+
* start: `align-self: flex-start` does both. An alignment the container or the box asked for is
|
|
504
|
+
* kept, since it stops the stretch already.
|
|
505
|
+
*/
|
|
506
|
+
function fitContent(node, props) {
|
|
507
|
+
const width = props['width'] === 'fit-content';
|
|
508
|
+
const height = props['height'] === 'fit-content';
|
|
509
|
+
if (!width && !height)
|
|
510
|
+
return;
|
|
511
|
+
if (width)
|
|
512
|
+
delete props['width'];
|
|
513
|
+
if (height)
|
|
514
|
+
delete props['height'];
|
|
515
|
+
const parent = layoutParent(node);
|
|
516
|
+
if (!parent)
|
|
517
|
+
return;
|
|
518
|
+
const container = containerOf(parent);
|
|
519
|
+
parent.fitContainer = container;
|
|
520
|
+
const [direction, align] = container.split(' ');
|
|
521
|
+
const across = direction === 'row' ? height : width;
|
|
522
|
+
if (across && align === 'stretch' && STRETCHES.has(props['alignSelf'])) {
|
|
523
|
+
props['alignSelf'] = 'flex-start';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const isPercent = (value) => typeof value === 'string' && value.endsWith('%');
|
|
527
|
+
/**
|
|
528
|
+
* Whether a box has a height a percentage can be taken of, as CSS defines one: a height of its
|
|
529
|
+
* own, or one the layout around it gives it. A box as tall as what it holds has none.
|
|
530
|
+
*
|
|
531
|
+
* Asked of each box on the way up, and remembered on it, so a change to any of them works out
|
|
532
|
+
* again the boxes beneath it. Where the answer is not known for certain it is yes, which leaves
|
|
533
|
+
* the percentage to Yoga as before: a view native sizes itself, or one placed out of the flow.
|
|
534
|
+
*/
|
|
535
|
+
function definiteHeight(node) {
|
|
536
|
+
return (node.heightBasis = hasDefiniteHeight(node));
|
|
537
|
+
}
|
|
538
|
+
function hasDefiniteHeight(node) {
|
|
539
|
+
// The root is the screen, and only a plain view is sized by its content alone.
|
|
540
|
+
if (!node.parent || node.kind !== 'element' || viewNameOf(node) !== 'View')
|
|
541
|
+
return true;
|
|
542
|
+
const parent = layoutParent(node);
|
|
543
|
+
if (!parent)
|
|
544
|
+
return true;
|
|
545
|
+
const height = ownLayout(node, 'height');
|
|
546
|
+
if (isPercent(height))
|
|
547
|
+
return definiteHeight(parent);
|
|
548
|
+
return sizesItself(node, height) || heightFromLayout(node, parent);
|
|
549
|
+
}
|
|
550
|
+
const NO_HEIGHT = new Set([undefined, null, 'auto', 'fit-content']);
|
|
551
|
+
/** Whether a box has a height from its own style: written, or placed, or kept in proportion. */
|
|
552
|
+
function sizesItself(node, height) {
|
|
553
|
+
if (!NO_HEIGHT.has(height))
|
|
554
|
+
return true;
|
|
555
|
+
return ownLayout(node, 'position') === 'absolute' || ownLayout(node, 'aspectRatio') != null;
|
|
556
|
+
}
|
|
557
|
+
/** Whether the container a box is in gives it a height: a row's, or a share of a column's. */
|
|
558
|
+
function heightFromLayout(node, parent) {
|
|
559
|
+
const [direction, align] = containerOf(parent).split(' ');
|
|
560
|
+
if (direction === 'row') {
|
|
561
|
+
// Stretched across a row, it is as tall as the row.
|
|
562
|
+
const self = ownLayout(node, 'alignSelf');
|
|
563
|
+
return STRETCHES.has(self) && align === 'stretch';
|
|
564
|
+
}
|
|
565
|
+
// In a column it has the height it grows to, where the column has one to share out.
|
|
566
|
+
const grows = Number(ownLayout(node, 'flexGrow') ?? ownLayout(node, 'flex') ?? 0) > 0;
|
|
567
|
+
return grows && definiteHeight(parent);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* `height: 50%` under a parent with no height to take it of, which CSS reads as `auto`. Yoga
|
|
571
|
+
* takes it of the space on offer, the screen's or everything a scroll view holds, so no height
|
|
572
|
+
* is sent.
|
|
573
|
+
*/
|
|
574
|
+
function percentHeight(node, props) {
|
|
575
|
+
if (!isPercent(props['height']))
|
|
576
|
+
return;
|
|
577
|
+
const parent = layoutParent(node);
|
|
578
|
+
if (parent && !definiteHeight(parent))
|
|
579
|
+
delete props['height'];
|
|
580
|
+
}
|
|
462
581
|
/**
|
|
463
582
|
* Start a multiline Android text field's text at the top, as iOS and a `<textarea>` do.
|
|
464
583
|
*
|
|
@@ -769,6 +888,30 @@ function holdsOnlyText(node) {
|
|
|
769
888
|
}
|
|
770
889
|
return true;
|
|
771
890
|
}
|
|
891
|
+
const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
|
|
892
|
+
/**
|
|
893
|
+
* Whether one of HTML's text elements is a flex container that aligns the one run of text it
|
|
894
|
+
* holds, as an avatar's initials are centred: `<span class="flex items-center justify-center">`.
|
|
895
|
+
*
|
|
896
|
+
* A browser makes the run a flex item and places it. A paragraph's text is its content, which no
|
|
897
|
+
* alignment moves, so such an element commits as a view, and its text as the paragraph any view's
|
|
898
|
+
* loose text is. Read from the node's own style on each reconcile, which a change to it brings
|
|
899
|
+
* about, so nothing is kept to go stale. A `<text>` is a paragraph whatever its style, and an
|
|
900
|
+
* element holding more than the one run is left as it was.
|
|
901
|
+
*/
|
|
902
|
+
function aligningText(node) {
|
|
903
|
+
if (node.kind !== 'element' || node.name === 'text' || !TEXT_ELEMENTS.has(node.name)) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
if (node.children.length !== 1 || node.children[0].kind !== 'text')
|
|
907
|
+
return false;
|
|
908
|
+
if (isTextElement(node.parent) || ownLayout(node, 'display') !== 'flex')
|
|
909
|
+
return false;
|
|
910
|
+
return (ALIGNS.has(ownLayout(node, 'alignItems')) ||
|
|
911
|
+
ALIGNS.has(ownLayout(node, 'justifyContent')));
|
|
912
|
+
}
|
|
913
|
+
/** The view a node is committed as: `viewNameOf`, but for a text element that aligns its text. */
|
|
914
|
+
const committedViewName = (node) => aligningText(node) ? DEFAULT_VIEW : viewNameOf(node);
|
|
772
915
|
/**
|
|
773
916
|
* The native view a node commits as: `Paragraph` for a `<text>`, `VirtualText` for one nested in
|
|
774
917
|
* another, `RawText` for a run of text, and `View` for anything unregistered, which is what an
|
|
@@ -795,7 +938,9 @@ export function viewNameOf(node) {
|
|
|
795
938
|
if (node.name !== 'text' && TEXT_ELEMENTS.has(node.name) && !holdsOnlyText(node)) {
|
|
796
939
|
return DEFAULT_VIEW;
|
|
797
940
|
}
|
|
798
|
-
|
|
941
|
+
// The paragraph the engine makes for a view's loose text is under a view, whatever the name
|
|
942
|
+
// of the element that view is.
|
|
943
|
+
return !node.anonymous && isTextElement(node.parent) ? VIRTUAL_TEXT : PARAGRAPH;
|
|
799
944
|
}
|
|
800
945
|
/**
|
|
801
946
|
* The props every native view reads as a boolean: the `bool` fields of React Native's
|
|
@@ -846,22 +991,47 @@ function wasOrIsEmptyWithout(node, moved) {
|
|
|
846
991
|
}
|
|
847
992
|
return true;
|
|
848
993
|
}
|
|
994
|
+
/**
|
|
995
|
+
* Have the child that paints a node's background merged again with the node: nothing of the
|
|
996
|
+
* child's own moved, and what it paints may have.
|
|
997
|
+
*/
|
|
998
|
+
function repaint(node) {
|
|
999
|
+
if (node.paintsOn?.parent === node)
|
|
1000
|
+
node.paintsOn.propsDirty = true;
|
|
1001
|
+
}
|
|
1002
|
+
/** A sixtieth of a second: how far apart the frames native is given for an animation are. */
|
|
1003
|
+
const NATIVE_FRAME = 1000 / 60;
|
|
1004
|
+
/** The style keys that are a view's background. */
|
|
1005
|
+
const PAINT_KEYS = [
|
|
1006
|
+
'backgroundColor',
|
|
1007
|
+
'experimental_backgroundImage',
|
|
1008
|
+
'experimental_backgroundSize',
|
|
1009
|
+
'experimental_backgroundPosition',
|
|
1010
|
+
'experimental_backgroundRepeat',
|
|
1011
|
+
];
|
|
849
1012
|
/**
|
|
850
1013
|
* A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
|
|
851
1014
|
* attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
|
|
852
|
-
* native reads.
|
|
1015
|
+
* native reads. The node's `attributeOnly` names stay on it the same way.
|
|
853
1016
|
*/
|
|
854
|
-
function writeOwnProps(own, props) {
|
|
1017
|
+
function writeOwnProps(own, props, attributeOnly) {
|
|
855
1018
|
let aria = false;
|
|
856
1019
|
for (const key of Object.keys(own)) {
|
|
1020
|
+
if (attributeOnly?.has(key))
|
|
1021
|
+
continue;
|
|
857
1022
|
if (key.includes('-'))
|
|
858
1023
|
aria ||= key.startsWith('aria-');
|
|
859
1024
|
else if (key !== 'style' && key !== INTRINSIC_SIZE && key !== STYLE_OVERRIDE) {
|
|
860
1025
|
props[key] = committedProp(key, own[key]);
|
|
861
1026
|
}
|
|
862
1027
|
}
|
|
863
|
-
if (aria)
|
|
864
|
-
|
|
1028
|
+
if (!aria)
|
|
1029
|
+
return;
|
|
1030
|
+
// An attribute-only `aria-*` is not mapped to the prop native reads either.
|
|
1031
|
+
const mapped = attributeOnly?.size
|
|
1032
|
+
? Object.fromEntries(Object.entries(own).filter(([key]) => !attributeOnly.has(key)))
|
|
1033
|
+
: own;
|
|
1034
|
+
applyAria(mapped, props);
|
|
865
1035
|
}
|
|
866
1036
|
/** A prop as it is committed: a boolean view prop held as text is the boolean the text says. */
|
|
867
1037
|
function committedProp(key, value) {
|
|
@@ -1039,6 +1209,10 @@ class RetainedNode {
|
|
|
1039
1209
|
styleCache = null;
|
|
1040
1210
|
styleDirty = true;
|
|
1041
1211
|
styleCommitted = null;
|
|
1212
|
+
fitContainer = undefined;
|
|
1213
|
+
heightBasis = undefined;
|
|
1214
|
+
hasDirty = false;
|
|
1215
|
+
stateDirty = false;
|
|
1042
1216
|
ownStyle = undefined;
|
|
1043
1217
|
claimed = undefined;
|
|
1044
1218
|
dormantHoists = null;
|
|
@@ -1182,12 +1356,19 @@ class RetainedNode {
|
|
|
1182
1356
|
* colours - `shouldRasterizeIOS` is a boolean.
|
|
1183
1357
|
*/
|
|
1184
1358
|
const IS_COLOR_PROP = /color(android)?$/i;
|
|
1359
|
+
/**
|
|
1360
|
+
* Colour props React Native does not name with a `color` suffix: `<switch>`'s pair ends in the
|
|
1361
|
+
* state each one paints - `trackColorForTrue` and `trackColorForFalse`. They are the same type
|
|
1362
|
+
* problem the `Android` suffix causes: Android's `ColorPropConverter` takes a number or a
|
|
1363
|
+
* platform-colour map, refuses a string, and throws rather than approximating one.
|
|
1364
|
+
*/
|
|
1365
|
+
const STATE_COLOR_PROPS = new Set(['trackColorForTrue', 'trackColorForFalse']);
|
|
1185
1366
|
/** Answers per key name, once: the regex ran for every prop of every node in every commit. */
|
|
1186
1367
|
const colorProps = new Map();
|
|
1187
1368
|
function isColorProp(key) {
|
|
1188
1369
|
let answer = colorProps.get(key);
|
|
1189
1370
|
if (answer === undefined) {
|
|
1190
|
-
colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || BRUSH_PROPS.has(key)));
|
|
1371
|
+
colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || STATE_COLOR_PROPS.has(key) || BRUSH_PROPS.has(key)));
|
|
1191
1372
|
}
|
|
1192
1373
|
return answer;
|
|
1193
1374
|
}
|
|
@@ -1431,6 +1612,8 @@ export class Engine {
|
|
|
1431
1612
|
* does, a child list can move without anything else needing to be looked at again.
|
|
1432
1613
|
*/
|
|
1433
1614
|
structuralSheets = false;
|
|
1615
|
+
/** Whether any sheet uses `:has()`. See `markBeneath`. */
|
|
1616
|
+
hasSheets = false;
|
|
1434
1617
|
resolveAssetSource;
|
|
1435
1618
|
constructor(fabric, rootTag, options = {}) {
|
|
1436
1619
|
this.fabric = fabric;
|
|
@@ -1442,6 +1625,8 @@ export class Engine {
|
|
|
1442
1625
|
if (options.tokens)
|
|
1443
1626
|
this.styles.setRootTokens(options.tokens);
|
|
1444
1627
|
this.structuralSheets = options.globalStyles?.structural === true;
|
|
1628
|
+
this.watchHas(options.globalStyles);
|
|
1629
|
+
this.watchActive(options.globalStyles);
|
|
1445
1630
|
this.dev = options.dev ?? globalThis.__DEV__ === true;
|
|
1446
1631
|
if (this.dev) {
|
|
1447
1632
|
this.styles.onUndefinedToken = (name, props, on) => this.reportUndefinedToken(name, props, on);
|
|
@@ -1506,11 +1691,70 @@ export class Engine {
|
|
|
1506
1691
|
if (node.active === active)
|
|
1507
1692
|
continue;
|
|
1508
1693
|
node.active = active;
|
|
1509
|
-
this.
|
|
1694
|
+
this.markActive(node);
|
|
1510
1695
|
changed = true;
|
|
1511
1696
|
}
|
|
1512
1697
|
return changed;
|
|
1513
1698
|
}
|
|
1699
|
+
/** Whether any sheet styles a pressed element, and each compound that asks about one from another. */
|
|
1700
|
+
activeUse = {
|
|
1701
|
+
own: false,
|
|
1702
|
+
elsewhere: [],
|
|
1703
|
+
};
|
|
1704
|
+
/** What each sheet in play asks about a pressed element: `activeUse` is all of them together. */
|
|
1705
|
+
activeBySheet = new Map();
|
|
1706
|
+
/** Note what a sheet in play asks about a pressed element. See `markActive`. */
|
|
1707
|
+
watchActive(sheet) {
|
|
1708
|
+
// Asked for every element a component's sheet is given to: noted once a sheet.
|
|
1709
|
+
if (!sheet || this.activeBySheet.has(sheet))
|
|
1710
|
+
return;
|
|
1711
|
+
const use = usesActive(sheet);
|
|
1712
|
+
this.activeBySheet.set(sheet, use);
|
|
1713
|
+
if (!use.own && !use.elsewhere.length)
|
|
1714
|
+
return;
|
|
1715
|
+
this.countActive();
|
|
1716
|
+
if (this.styles.tracksHas)
|
|
1717
|
+
return;
|
|
1718
|
+
// What a pressed element is compared with is the rules it matched, which no cache kept until
|
|
1719
|
+
// a sheet asked. Nothing is resolved yet when the sheet is the one the engine was made with.
|
|
1720
|
+
this.styles.tracksHas = true;
|
|
1721
|
+
if (!this.root)
|
|
1722
|
+
return;
|
|
1723
|
+
this.root.styleDirty = true;
|
|
1724
|
+
this.markPath(this.root);
|
|
1725
|
+
}
|
|
1726
|
+
/** A sheet has left play, by a hot swap or by being removed: what it asked goes with it. */
|
|
1727
|
+
unwatchActive(sheet) {
|
|
1728
|
+
if (this.activeBySheet.delete(sheet))
|
|
1729
|
+
this.countActive();
|
|
1730
|
+
}
|
|
1731
|
+
countActive() {
|
|
1732
|
+
const all = [...this.activeBySheet.values()];
|
|
1733
|
+
this.activeUse = {
|
|
1734
|
+
own: all.some((use) => use.own),
|
|
1735
|
+
elsewhere: all.flatMap((use) => use.elsewhere),
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* A press began or ended on `node` or under it, which is all that changed about it. A press
|
|
1740
|
+
* reaches every ancestor up to the root, and marking each as restyled would style the whole
|
|
1741
|
+
* screen again, twice a press. So each is only matched again, and keeps its style and all
|
|
1742
|
+
* that is under it unless it now matches other rules. An element a rule asks about from
|
|
1743
|
+
* inside or beside it, `.card:active .title`, changes more than itself, and is restyled with
|
|
1744
|
+
* everything under it as any other change is. With no rule for a pressed element at all there
|
|
1745
|
+
* is nothing to match.
|
|
1746
|
+
*/
|
|
1747
|
+
markActive(node) {
|
|
1748
|
+
const { own, elsewhere } = this.activeUse;
|
|
1749
|
+
if (!own && !elsewhere.length)
|
|
1750
|
+
return;
|
|
1751
|
+
// By its classes alone: a compound with none could be any element.
|
|
1752
|
+
const asked = elsewhere.some((compound) => compound.classes.every((name) => node.classes?.has(name)));
|
|
1753
|
+
if (asked)
|
|
1754
|
+
return this.markProps(node);
|
|
1755
|
+
node.stateDirty = true;
|
|
1756
|
+
this.markPath(node.parent ?? node);
|
|
1757
|
+
}
|
|
1514
1758
|
/**
|
|
1515
1759
|
* Custom properties the device knows and the stylesheet cannot, once they change.
|
|
1516
1760
|
*
|
|
@@ -1533,6 +1777,8 @@ export class Engine {
|
|
|
1533
1777
|
this.registerSheet(sheet);
|
|
1534
1778
|
if (sheet.structural)
|
|
1535
1779
|
this.structuralSheets = true;
|
|
1780
|
+
this.watchHas(sheet);
|
|
1781
|
+
this.watchActive(sheet);
|
|
1536
1782
|
this.markPath(this.root);
|
|
1537
1783
|
return true;
|
|
1538
1784
|
}
|
|
@@ -1563,14 +1809,19 @@ export class Engine {
|
|
|
1563
1809
|
* which is what makes Fabric dirty a measured node and measure it at the size now in effect.
|
|
1564
1810
|
*/
|
|
1565
1811
|
remeasureText() {
|
|
1812
|
+
const again = (measured) => {
|
|
1813
|
+
measured.remeasure = true;
|
|
1814
|
+
this.markProps(measured, false);
|
|
1815
|
+
};
|
|
1566
1816
|
const visit = (node) => {
|
|
1567
1817
|
for (const child of node.children) {
|
|
1818
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
1819
|
+
if (child.kind === 'text' && child.box?.committed)
|
|
1820
|
+
again(child.box);
|
|
1568
1821
|
if (child.kind !== 'element')
|
|
1569
1822
|
continue;
|
|
1570
|
-
if (child.committed && MEASURED_VIEWS.has(
|
|
1571
|
-
child
|
|
1572
|
-
this.markProps(child, false);
|
|
1573
|
-
}
|
|
1823
|
+
if (child.committed && MEASURED_VIEWS.has(committedViewName(child)))
|
|
1824
|
+
again(child);
|
|
1574
1825
|
visit(child);
|
|
1575
1826
|
}
|
|
1576
1827
|
};
|
|
@@ -1718,6 +1969,8 @@ export class Engine {
|
|
|
1718
1969
|
node.sheet = sheet;
|
|
1719
1970
|
if (sheet?.structural)
|
|
1720
1971
|
this.structuralSheets = true;
|
|
1972
|
+
this.watchHas(sheet);
|
|
1973
|
+
this.watchActive(sheet);
|
|
1721
1974
|
this.markProps(node);
|
|
1722
1975
|
}
|
|
1723
1976
|
/** The sheet each node given another's scope was created with. */
|
|
@@ -1758,6 +2011,8 @@ export class Engine {
|
|
|
1758
2011
|
}
|
|
1759
2012
|
if (sheet?.structural)
|
|
1760
2013
|
this.structuralSheets = true;
|
|
2014
|
+
this.watchHas(sheet);
|
|
2015
|
+
this.watchActive(sheet);
|
|
1761
2016
|
if (HOISTS[name])
|
|
1762
2017
|
this.hoisted.add(node);
|
|
1763
2018
|
return node;
|
|
@@ -1779,6 +2034,7 @@ export class Engine {
|
|
|
1779
2034
|
if (node.hostSheet === sheet)
|
|
1780
2035
|
return;
|
|
1781
2036
|
node.hostSheet = sheet;
|
|
2037
|
+
this.watchActive(sheet);
|
|
1782
2038
|
this.markProps(node);
|
|
1783
2039
|
}
|
|
1784
2040
|
/**
|
|
@@ -1894,6 +2150,7 @@ export class Engine {
|
|
|
1894
2150
|
node.playing = undefined;
|
|
1895
2151
|
this.markProps(node, false);
|
|
1896
2152
|
}
|
|
2153
|
+
this.releasePlayedNatively();
|
|
1897
2154
|
for (const nodes of this.scrollTimelines.values()) {
|
|
1898
2155
|
for (const node of nodes) {
|
|
1899
2156
|
if (this.topOf(node) === this.root)
|
|
@@ -1928,8 +2185,14 @@ export class Engine {
|
|
|
1928
2185
|
destroyNode(node) {
|
|
1929
2186
|
node.listeners?.clear();
|
|
1930
2187
|
node.listeners = null;
|
|
1931
|
-
node
|
|
2188
|
+
// With what it holds: a node Angular destroys can be put back, as content projected into
|
|
2189
|
+
// an `@if` is, and its children's views would then be handed to the view it is made again
|
|
2190
|
+
// as. Native gives a view one parent for its life, and aborts on a second.
|
|
2191
|
+
if (node.committed)
|
|
2192
|
+
this.forgetCommitted(node);
|
|
1932
2193
|
node.transitions = undefined;
|
|
2194
|
+
if (node.playing)
|
|
2195
|
+
this.stopNative(node, node.playing);
|
|
1933
2196
|
node.playing = undefined;
|
|
1934
2197
|
this.stopScrolled(node);
|
|
1935
2198
|
this.running.delete(node);
|
|
@@ -1950,6 +2213,9 @@ export class Engine {
|
|
|
1950
2213
|
return siblings[siblings.indexOf(node) + 1] ?? null;
|
|
1951
2214
|
}
|
|
1952
2215
|
setProp(node, key, value) {
|
|
2216
|
+
// A prop written again is a binding's, and is committed as any other is.
|
|
2217
|
+
if (node.attributeOnly?.delete(key))
|
|
2218
|
+
this.markProps(node, false);
|
|
1953
2219
|
if (value === undefined || value === null) {
|
|
1954
2220
|
// Nothing to remove, so nothing changed. Worth its own branch because it is the common
|
|
1955
2221
|
// case, not a rare one: a host primitive carries a host binding for every prop React Native
|
|
@@ -1969,6 +2235,29 @@ export class Engine {
|
|
|
1969
2235
|
// inline `direction` is the exception, because the paragraphs below it align by it.
|
|
1970
2236
|
this.markProps(node, key !== 'style' || this.inlineReachesStyle(node));
|
|
1971
2237
|
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
2240
|
+
* `background-color` and `background-image` in the cascade, where a stylesheet wrote them, and
|
|
2241
|
+
* the child is the view that draws them. What a view that masks its children and not itself
|
|
2242
|
+
* needs, as `<gradient-text>` is.
|
|
2243
|
+
*/
|
|
2244
|
+
paintOn(node, child) {
|
|
2245
|
+
node.paintsOn = child;
|
|
2246
|
+
this.markProps(node, false);
|
|
2247
|
+
this.markProps(child, false);
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Keep a prop the node already has as an attribute: on the node for a selector such as
|
|
2251
|
+
* `:host([variant='primary'])` to match, and left out of what is committed to the native view.
|
|
2252
|
+
* It is committed again once the prop is next written. What the platform adapter does with a
|
|
2253
|
+
* static attribute that is an input of the host's component.
|
|
2254
|
+
*/
|
|
2255
|
+
keepAsAttribute(node, key) {
|
|
2256
|
+
if (!(key in node.props))
|
|
2257
|
+
return;
|
|
2258
|
+
(node.attributeOnly ??= new Set()).add(key);
|
|
2259
|
+
this.markProps(node, false);
|
|
2260
|
+
}
|
|
1972
2261
|
/**
|
|
1973
2262
|
* Whether a node's inline style can change what it or anything below it resolves to: it sets,
|
|
1974
2263
|
* or until now set, a property its children inherit.
|
|
@@ -2072,11 +2361,35 @@ export class Engine {
|
|
|
2072
2361
|
// state, though nothing about the sibling moved. Only sheets that ask about siblings pay.
|
|
2073
2362
|
if (this.structuralSheets)
|
|
2074
2363
|
this.markLaterSiblings(node);
|
|
2364
|
+
if (this.hasSheets)
|
|
2365
|
+
this.markBeneath(node.parent);
|
|
2075
2366
|
}
|
|
2076
2367
|
// The root has no parent to carry the mark, so it carries its own: `.dark` on the root alone
|
|
2077
2368
|
// restyles everything beneath it.
|
|
2078
2369
|
this.markPath(node.parent ?? node);
|
|
2079
2370
|
}
|
|
2371
|
+
/** Start matching ancestors again on a change, once a sheet that uses `:has()` is in play. */
|
|
2372
|
+
watchHas(sheet) {
|
|
2373
|
+
if (!sheet?.has || this.hasSheets)
|
|
2374
|
+
return;
|
|
2375
|
+
this.hasSheets = true;
|
|
2376
|
+
this.styles.tracksHas = true;
|
|
2377
|
+
// What is already resolved kept no record of the rules it matched: resolve it again. Nothing
|
|
2378
|
+
// is, when the sheet is the one the engine was made with.
|
|
2379
|
+
if (!this.root)
|
|
2380
|
+
return;
|
|
2381
|
+
this.root.styleDirty = true;
|
|
2382
|
+
this.markPath(this.root);
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Something at or beneath `from` changed: every node from there up may match a `:has()` rule
|
|
2386
|
+
* it did not, or stop matching one. They are matched again rather than marked for restyling,
|
|
2387
|
+
* which would restyle everything under them. Only sheets that use `:has()` pay.
|
|
2388
|
+
*/
|
|
2389
|
+
markBeneath(from) {
|
|
2390
|
+
for (let node = from; node; node = node.parent)
|
|
2391
|
+
node.hasDirty = true;
|
|
2392
|
+
}
|
|
2080
2393
|
markLaterSiblings(node) {
|
|
2081
2394
|
const siblings = node.parent?.children;
|
|
2082
2395
|
if (!siblings)
|
|
@@ -2099,6 +2412,8 @@ export class Engine {
|
|
|
2099
2412
|
if (wasOrIsEmptyWithout(node, moved))
|
|
2100
2413
|
this.markLaterSiblings(node);
|
|
2101
2414
|
}
|
|
2415
|
+
if (this.hasSheets)
|
|
2416
|
+
this.markBeneath(node);
|
|
2102
2417
|
this.markPath(node.parent);
|
|
2103
2418
|
}
|
|
2104
2419
|
/** Walk up until we hit a node already known to have a dirty subtree. */
|
|
@@ -2304,11 +2619,17 @@ export class Engine {
|
|
|
2304
2619
|
const props = { ...DEFAULT_PROPS[viewName], ...node.defaultStyle };
|
|
2305
2620
|
Object.assign(props, this.styles.resolve(node, this.styleEpoch).style);
|
|
2306
2621
|
const resolved = props['pointerEvents'];
|
|
2307
|
-
writeOwnProps(node.props, props);
|
|
2622
|
+
writeOwnProps(node.props, props, node.attributeOnly);
|
|
2308
2623
|
withTextContent(node, viewName, props);
|
|
2309
2624
|
const cascaded = props['transform'];
|
|
2310
2625
|
const style = boundTransform(node, flattenStyle(node.props['style'], props), cascaded);
|
|
2626
|
+
if (node.props['style']) {
|
|
2627
|
+
this.styles.overOtherForms(flattenStyle(node.props['style'], {}), style);
|
|
2628
|
+
}
|
|
2311
2629
|
nativePointerEvents(node, style, resolved);
|
|
2630
|
+
// Before an image's own size: `fit-content` is no size, so the image's is what it gets.
|
|
2631
|
+
fitContent(node, style);
|
|
2632
|
+
percentHeight(node, style);
|
|
2312
2633
|
const intrinsic = node.props[INTRINSIC_SIZE];
|
|
2313
2634
|
if (intrinsic)
|
|
2314
2635
|
applyIntrinsicSize(style, intrinsic);
|
|
@@ -2322,8 +2643,53 @@ export class Engine {
|
|
|
2322
2643
|
alignMultiline(viewName, style);
|
|
2323
2644
|
const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
2324
2645
|
centreSingleLine(viewName, merged, this.fontScale);
|
|
2646
|
+
this.movePaint(node, merged);
|
|
2325
2647
|
return merged;
|
|
2326
2648
|
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Leave a node's background out of what it commits when a child paints it, and give that child
|
|
2651
|
+
* its parent's: the cascade's, and over it the parent's inline style.
|
|
2652
|
+
*/
|
|
2653
|
+
movePaint(node, merged) {
|
|
2654
|
+
if (node.paintsOn)
|
|
2655
|
+
for (const key of PAINT_KEYS)
|
|
2656
|
+
delete merged[key];
|
|
2657
|
+
const from = node.parent?.paintsOn === node ? node.parent : null;
|
|
2658
|
+
if (!from)
|
|
2659
|
+
return;
|
|
2660
|
+
const paint = flattenStyle(from.props['style'], {
|
|
2661
|
+
...this.styles.resolve(from, this.styleEpoch).style,
|
|
2662
|
+
});
|
|
2663
|
+
for (const key of PAINT_KEYS)
|
|
2664
|
+
if (paint[key] !== undefined)
|
|
2665
|
+
merged[key] = paint[key];
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* A container whose direction or alignment changed since a child with a `fit-content` size
|
|
2669
|
+
* read it, or whose height stopped or started being one a percentage is taken of since a child
|
|
2670
|
+
* with a percentage height asked: those children are merged again. A change to a container
|
|
2671
|
+
* marks nothing beneath it, since none of these is inherited.
|
|
2672
|
+
*/
|
|
2673
|
+
refitChildren(node) {
|
|
2674
|
+
// Both are asked, and both remembered, before either has the children merged again.
|
|
2675
|
+
const basis = node.heightBasis;
|
|
2676
|
+
const based = basis !== undefined && definiteHeight(node) !== basis;
|
|
2677
|
+
const fitted = node.fitContainer !== undefined && containerOf(node) !== node.fitContainer;
|
|
2678
|
+
if (fitted)
|
|
2679
|
+
node.fitContainer = containerOf(node);
|
|
2680
|
+
if (based || fitted)
|
|
2681
|
+
this.refit(node);
|
|
2682
|
+
}
|
|
2683
|
+
/** Have a container's children merged again, through any `display: contents` among them. */
|
|
2684
|
+
refit(container) {
|
|
2685
|
+
for (const child of container.children) {
|
|
2686
|
+
if (child.kind !== 'element')
|
|
2687
|
+
continue;
|
|
2688
|
+
child.propsDirty = true;
|
|
2689
|
+
if (containerOf(child) === 'contents')
|
|
2690
|
+
this.refit(child);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2327
2693
|
/**
|
|
2328
2694
|
* The text size cap that lays a paragraph or text input out again once a face it names has
|
|
2329
2695
|
* registered: a step per face above the app's own cap where it set one (below 1 is none, on
|
|
@@ -2397,6 +2763,8 @@ export class Engine {
|
|
|
2397
2763
|
sheetReplaced(sheet, next) {
|
|
2398
2764
|
if (sheet === next && this.knownSheets.has(sheet))
|
|
2399
2765
|
return;
|
|
2766
|
+
this.unwatchActive(sheet);
|
|
2767
|
+
this.watchActive(next);
|
|
2400
2768
|
const at = this.sheetOrder.indexOf(sheet);
|
|
2401
2769
|
if (at === -1) {
|
|
2402
2770
|
this.registerSheet(next);
|
|
@@ -2459,11 +2827,17 @@ export class Engine {
|
|
|
2459
2827
|
}
|
|
2460
2828
|
/** Mark every element under `node` committed with a family, so it is matched again. */
|
|
2461
2829
|
markFontText(node) {
|
|
2830
|
+
const named = (text) => {
|
|
2831
|
+
if (typeof text?.committed?.props['fontFamily'] === 'string')
|
|
2832
|
+
this.markProps(text, false);
|
|
2833
|
+
};
|
|
2462
2834
|
for (const child of node.children) {
|
|
2835
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
2836
|
+
if (child.kind === 'text')
|
|
2837
|
+
named(child.box);
|
|
2463
2838
|
if (child.kind !== 'element')
|
|
2464
2839
|
continue;
|
|
2465
|
-
|
|
2466
|
-
this.markProps(child, false);
|
|
2840
|
+
named(child);
|
|
2467
2841
|
this.markFontText(child);
|
|
2468
2842
|
}
|
|
2469
2843
|
}
|
|
@@ -2510,6 +2884,7 @@ export class Engine {
|
|
|
2510
2884
|
handOverToScroll(node, spec) {
|
|
2511
2885
|
if (!node.playing)
|
|
2512
2886
|
return;
|
|
2887
|
+
this.stopNative(node, node.playing);
|
|
2513
2888
|
if (node.playing.spec.name !== spec.name)
|
|
2514
2889
|
this.cancelPlaying(node);
|
|
2515
2890
|
node.playing = undefined;
|
|
@@ -2521,9 +2896,88 @@ export class Engine {
|
|
|
2521
2896
|
*/
|
|
2522
2897
|
cancelPlaying(node) {
|
|
2523
2898
|
const running = node.playing;
|
|
2899
|
+
if (running)
|
|
2900
|
+
this.stopNative(node, running);
|
|
2524
2901
|
if (running && !running.done)
|
|
2525
2902
|
this.emitTransition(node, 'topAnimationcancel', running.spec.name);
|
|
2526
2903
|
}
|
|
2904
|
+
/** Stop on native each animation whose view has left the tree. */
|
|
2905
|
+
releasePlayedNatively() {
|
|
2906
|
+
for (const node of [...this.playedNatively]) {
|
|
2907
|
+
if (this.topOf(node) === this.root)
|
|
2908
|
+
continue;
|
|
2909
|
+
this.stopNative(node, node.playing);
|
|
2910
|
+
node.playing = undefined;
|
|
2911
|
+
this.markProps(node, false);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
/** Every node whose animation native is playing, with no frame of it in JavaScript. */
|
|
2915
|
+
playedNatively = new Set();
|
|
2916
|
+
/**
|
|
2917
|
+
* Hand an animation that has just started to native, where native can play it: one that moves
|
|
2918
|
+
* only opacity and transforms, starts at once, and repeats a whole number of times. Native then
|
|
2919
|
+
* runs it frame by frame, and JavaScript hears of it again when it ends. Anything else is
|
|
2920
|
+
* played from here, a commit a frame. See `NativeScrollDriver.play`.
|
|
2921
|
+
*/
|
|
2922
|
+
playNatively(node, running, props) {
|
|
2923
|
+
const { spec } = running;
|
|
2924
|
+
if (!this.scrollDriver || spec.delay !== 0 || spec.duration <= 0)
|
|
2925
|
+
return false;
|
|
2926
|
+
const { channels, held, span } = clockChannels(running.tracks, spec, props);
|
|
2927
|
+
if (held.length || !(channels.opacity || channels.transform.length))
|
|
2928
|
+
return false;
|
|
2929
|
+
// There and back is one native animation: an odd number of ways has no whole number of them.
|
|
2930
|
+
const count = spec.iterations === null ? -1 : spec.iterations / span;
|
|
2931
|
+
if (count !== -1 && !(Number.isInteger(count) && count > 0))
|
|
2932
|
+
return false;
|
|
2933
|
+
const length = Math.max(2, Math.round((spec.duration * span) / NATIVE_FRAME) + 1);
|
|
2934
|
+
const frames = Array.from({ length }, (_, i) => i / (length - 1));
|
|
2935
|
+
const native = this.scrollDriver.play(node, channels, { frames, toValue: span, iterations: count }, (view) => this.tagOf(view), () => this.endedNatively(node, running));
|
|
2936
|
+
if (!native)
|
|
2937
|
+
return false;
|
|
2938
|
+
running.native = native;
|
|
2939
|
+
// As Animated does: Fabric flattens a view that only lays out, and then there is no native
|
|
2940
|
+
// view for the animation to move.
|
|
2941
|
+
running.values = { ...running.values, collapsable: false };
|
|
2942
|
+
this.playedNatively.add(node);
|
|
2943
|
+
return true;
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Native reached the end of an animation it was playing. The frame it ends at, or the resting
|
|
2947
|
+
* style where it does not fill, is committed before the view is let go, so nothing shows of
|
|
2948
|
+
* the frame it started at, which is what the view had been committed with.
|
|
2949
|
+
*/
|
|
2950
|
+
endedNatively(node, running) {
|
|
2951
|
+
if (node.playing !== running || !running.native)
|
|
2952
|
+
return;
|
|
2953
|
+
const { native, spec } = running;
|
|
2954
|
+
running.native = undefined;
|
|
2955
|
+
this.playedNatively.delete(node);
|
|
2956
|
+
const end = running.start + spec.duration * (spec.iterations ?? 1);
|
|
2957
|
+
this.advancePlayer(node, Math.max(this.now(), end));
|
|
2958
|
+
this.commit();
|
|
2959
|
+
native.stop();
|
|
2960
|
+
}
|
|
2961
|
+
/** Take an animation back from native, which stops it there. The caller plays it on, or not. */
|
|
2962
|
+
stopNative(node, running) {
|
|
2963
|
+
if (!running.native)
|
|
2964
|
+
return;
|
|
2965
|
+
running.native.stop();
|
|
2966
|
+
running.native = undefined;
|
|
2967
|
+
this.playedNatively.delete(node);
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Play on from JavaScript an animation native was playing, from where its clock has got to:
|
|
2971
|
+
* what a pause, other frames or a view made again need, none of which native can be told of.
|
|
2972
|
+
*/
|
|
2973
|
+
backToScript(node, running) {
|
|
2974
|
+
if (!running.native)
|
|
2975
|
+
return;
|
|
2976
|
+
this.stopNative(node, running);
|
|
2977
|
+
running.values = sample(running, this.now()).values;
|
|
2978
|
+
if (!running.done && running.pausedAt === undefined)
|
|
2979
|
+
this.playing.add(node);
|
|
2980
|
+
}
|
|
2527
2981
|
/** Start the clock on an animation, unless the node is already playing this one. */
|
|
2528
2982
|
startPlaying(node, spec, frames, props) {
|
|
2529
2983
|
const current = node.playing;
|
|
@@ -2531,6 +2985,8 @@ export class Engine {
|
|
|
2531
2985
|
if (!current || !sameAnimation(current.spec, spec)) {
|
|
2532
2986
|
if (current && current.spec.name !== spec.name)
|
|
2533
2987
|
this.cancelPlaying(node);
|
|
2988
|
+
else if (current)
|
|
2989
|
+
this.stopNative(node, current);
|
|
2534
2990
|
const started = {
|
|
2535
2991
|
spec,
|
|
2536
2992
|
tracks: tracksOf(frames, props, inherited),
|
|
@@ -2551,12 +3007,16 @@ export class Engine {
|
|
|
2551
3007
|
node.playing = started;
|
|
2552
3008
|
if (spec.paused)
|
|
2553
3009
|
started.pausedAt = this.now();
|
|
2554
|
-
else
|
|
3010
|
+
else if (!this.playNatively(node, started, props))
|
|
2555
3011
|
this.playing.add(node);
|
|
2556
3012
|
this.emitTransition(node, 'topAnimationstart', spec.name);
|
|
2557
3013
|
return;
|
|
2558
3014
|
}
|
|
3015
|
+
// A view made again is another view: the one native was moving is gone.
|
|
3016
|
+
if (node.committed === null)
|
|
3017
|
+
this.backToScript(node, current);
|
|
2559
3018
|
if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
|
|
3019
|
+
this.backToScript(node, current);
|
|
2560
3020
|
this.reframe(node, current, frames, props, inherited);
|
|
2561
3021
|
}
|
|
2562
3022
|
this.playOrPause(node, node.playing, spec);
|
|
@@ -2594,6 +3054,7 @@ export class Engine {
|
|
|
2594
3054
|
current.spec = spec;
|
|
2595
3055
|
const now = this.now();
|
|
2596
3056
|
if (spec.paused && current.pausedAt === undefined && !current.done) {
|
|
3057
|
+
this.backToScript(node, current);
|
|
2597
3058
|
current.pausedAt = now;
|
|
2598
3059
|
this.playing.delete(node);
|
|
2599
3060
|
}
|
|
@@ -2904,7 +3365,8 @@ export class Engine {
|
|
|
2904
3365
|
*
|
|
2905
3366
|
* A stop's colour is a colour like any other, but it is two levels down and nothing else looks
|
|
2906
3367
|
* there: without this the device is handed the string `rgb(255, 0, 0)` where it wants a number,
|
|
2907
|
-
* and paints nothing.
|
|
3368
|
+
* and paints nothing. The stops are first rewritten so native's interpolation paints what
|
|
3369
|
+
* CSS's premultiplied one does (see `premultipliedStops`).
|
|
2908
3370
|
*/
|
|
2909
3371
|
processGradients(value) {
|
|
2910
3372
|
if (!Array.isArray(value))
|
|
@@ -2915,7 +3377,7 @@ export class Engine {
|
|
|
2915
3377
|
return entry;
|
|
2916
3378
|
return {
|
|
2917
3379
|
...gradient,
|
|
2918
|
-
colorStops: gradient.colorStops.map((stop) => ({
|
|
3380
|
+
colorStops: premultipliedStops(gradient.colorStops).map((stop) => ({
|
|
2919
3381
|
...stop,
|
|
2920
3382
|
color: this.color(stop.color),
|
|
2921
3383
|
})),
|
|
@@ -2937,7 +3399,9 @@ export class Engine {
|
|
|
2937
3399
|
if (this.isClean(node) && !styleChanged)
|
|
2938
3400
|
return node.committed.handle;
|
|
2939
3401
|
node.styleCommitted = style;
|
|
2940
|
-
|
|
3402
|
+
repaint(node);
|
|
3403
|
+
this.refitChildren(node);
|
|
3404
|
+
const viewName = committedViewName(node);
|
|
2941
3405
|
this.forgetRenamed(node, viewName);
|
|
2942
3406
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
2943
3407
|
noteTouches(node, style);
|
|
@@ -3276,7 +3740,10 @@ export class Engine {
|
|
|
3276
3740
|
* layout bug. On the first commit of any known element with no component behind it, say so.
|
|
3277
3741
|
*/
|
|
3278
3742
|
checkClaimed(node) {
|
|
3279
|
-
|
|
3743
|
+
// The paragraph the engine wraps loose text in is its own: no template wrote it.
|
|
3744
|
+
if (node.kind !== 'element' || node.claimed || node.anonymous)
|
|
3745
|
+
return;
|
|
3746
|
+
if (!PRIMITIVE_NAMES.has(node.name))
|
|
3280
3747
|
return;
|
|
3281
3748
|
if (this.reported.has(node.name))
|
|
3282
3749
|
return;
|