@ng-native/fabric 0.13.0 → 0.15.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.
Files changed (41) hide show
  1. package/dist/css.d.ts +63 -6
  2. package/dist/css.js +268 -59
  3. package/dist/css.js.map +1 -1
  4. package/dist/defer-triggers.js +2 -8
  5. package/dist/defer-triggers.js.map +1 -1
  6. package/dist/engine.d.ts +201 -21
  7. package/dist/engine.js +915 -125
  8. package/dist/engine.js.map +1 -1
  9. package/dist/fabric.d.ts +1 -1
  10. package/dist/fabric.js +25 -0
  11. package/dist/fabric.js.map +1 -1
  12. package/dist/font-faces.d.ts +19 -0
  13. package/dist/font-faces.js +42 -0
  14. package/dist/font-faces.js.map +1 -1
  15. package/dist/forms-inputs.d.ts +1 -1
  16. package/dist/forms-inputs.js +2 -2
  17. package/dist/forms-inputs.js.map +1 -1
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.js +2 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/inline-derived-token.js +56 -7
  22. package/dist/inline-derived-token.js.map +1 -1
  23. package/dist/inline-token.d.ts +4 -3
  24. package/dist/inline-token.js +41 -8
  25. package/dist/inline-token.js.map +1 -1
  26. package/dist/native-drive.d.ts +42 -1
  27. package/dist/native-drive.js +135 -9
  28. package/dist/native-drive.js.map +1 -1
  29. package/dist/node-global.d.ts +9 -0
  30. package/dist/node-global.js +41 -0
  31. package/dist/node-global.js.map +1 -0
  32. package/dist/relative-colour.d.ts +2 -2
  33. package/dist/relative-colour.js +4 -0
  34. package/dist/relative-colour.js.map +1 -1
  35. package/dist/scroll-animation.d.ts +12 -0
  36. package/dist/scroll-animation.js +10 -0
  37. package/dist/scroll-animation.js.map +1 -1
  38. package/dist/transition.d.ts +3 -2
  39. package/dist/transition.js +17 -8
  40. package/dist/transition.js.map +1 -1
  41. 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, hasAbove, placeReadElsewhere, siblingReach, } from "./css.js";
10
+ import { boundDeclaration, boundRelativeLength, CSS_SPACE, isNegativeLineHeight, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, joinSubjects, 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";
@@ -15,7 +15,7 @@ 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 { clockChannels, colourChannels, firstFrame, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
18
+ import { clockChannels, colourChannels, firstFrame, heldTransform, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
19
19
  import { FontFaces } from "./font-faces.js";
20
20
  import { premultipliedStops } from "./premultiplied-stops.js";
21
21
  /**
@@ -189,6 +189,13 @@ const VIEW_EVENT_OPT_IN_PROPS = {
189
189
  topError: 'shouldNotifyLoadEvents',
190
190
  topProgress: 'shouldNotifyLoadEvents',
191
191
  },
192
+ // `ReactTextInputManager` watches each of these only behind a prop of the handler's name,
193
+ // which `TextInput.js` sets by passing the handler through. iOS's field sends them unasked.
194
+ AndroidTextInput: {
195
+ topContentSizeChange: 'onContentSizeChange',
196
+ topScroll: 'onScroll',
197
+ topKeyPress: 'onKeyPress',
198
+ },
192
199
  };
193
200
  /**
194
201
  * Props the engine itself writes or reads off a node, so never a misspelling: the style, the
@@ -461,10 +468,14 @@ const blockEdge = (props, side, logical) => [
461
468
  /**
462
469
  * A multiline field's `rows` as its height, where it was given none: that many lines of its
463
470
  * line height at the system text size, inside its padding and border, as a browser sizes a `<textarea>`. A native field
464
- * is as tall as its text, which is one line before anything is typed.
471
+ * is as tall as its text, which is one line before anything is typed. Taken off the props once
472
+ * read: neither platform's native field has a prop of that name.
465
473
  */
466
- function rowsTall(props, fontScale) {
474
+ function rowsTall(viewName, props, fontScale) {
475
+ if (!TEXT_INPUTS.has(viewName))
476
+ return;
467
477
  const rows = Number(props['rows']);
478
+ delete props['rows'];
468
479
  const line = props['lineHeight'];
469
480
  if (props['multiline'] !== true || !(rows > 0) || typeof line !== 'number')
470
481
  return;
@@ -837,6 +848,12 @@ function placeholderFaded(props) {
837
848
  else if (opacity <= 0)
838
849
  props['placeholderTextColor'] = 'rgba(0, 0, 0, 0)';
839
850
  }
851
+ /**
852
+ * What a removed prop travels as: null, which native resets the prop on, except `pointerEvents`,
853
+ * which react-native-svg's Android root reads as a string and stops the app on a null. `auto` is
854
+ * what no value means.
855
+ */
856
+ const cleared = (key) => (key === 'pointerEvents' ? 'auto' : null);
840
857
  /** The least alpha iOS sends a touch to a view at: under a hundredth it is passed over. */
841
858
  const TOUCHED_ALPHA = 0.011;
842
859
  /**
@@ -854,6 +871,133 @@ function stillTouched(props) {
854
871
  props['opacity'] = TOUCHED_ALPHA;
855
872
  return props;
856
873
  }
874
+ /** Every prop `setOwned` can set, as a payload that changes them all. */
875
+ const RESET_OWNED = { opacity: undefined, transform: undefined };
876
+ /** As Animated does: a view native sets values on is kept, or there is no view to set them on. */
877
+ function keptForNative(node, props) {
878
+ if (node.owned && props['collapsable'] === undefined)
879
+ props['collapsable'] = false;
880
+ }
881
+ /**
882
+ * How a paragraph is truncated, which native does by its props: one the engine made around a
883
+ * view's own words is truncated as the view says, as a browser truncates a block's own text.
884
+ */
885
+ const TRUNCATION = ['numberOfLines', 'ellipsizeMode'];
886
+ /**
887
+ * The truncation each paragraph made around a view's words was merged with, as the view said it.
888
+ * Not what it was committed with, which has the paragraph's own defaults in it.
889
+ */
890
+ const truncatedBy = new WeakMap();
891
+ /** What a view says of how its own words are truncated. */
892
+ const truncationOf = (parent) => TRUNCATION.map((key) => ownLayout(parent, key) ?? null);
893
+ /** Whether a paragraph made around a view's words is truncated other than the view now says. */
894
+ function truncationMoved(box, parent) {
895
+ const merged = truncatedBy.get(box);
896
+ if (!merged)
897
+ return false;
898
+ return truncationOf(parent).some((said, at) => said !== merged[at]);
899
+ }
900
+ function truncatedAs(box, props) {
901
+ if (!box.anonymous || !box.parent)
902
+ return;
903
+ const said = truncationOf(box.parent);
904
+ truncatedBy.set(box, said);
905
+ TRUNCATION.forEach((key, at) => {
906
+ if (said[at] !== null)
907
+ props[key] = said[at];
908
+ });
909
+ }
910
+ /**
911
+ * Stretch the lines of a wrapping flex container that says nothing of `align-content`, as CSS
912
+ * starts them (`normal`, which is `stretch` in a flex container). Yoga starts them at
913
+ * `flex-start`, as tall as what is in them, where an item that stretches across its line has
914
+ * no size. A container that does not wrap has one line, which Yoga stretches already.
915
+ */
916
+ function wrappedLinesStretch(props) {
917
+ const wraps = props['flexWrap'] === 'wrap' || props['flexWrap'] === 'wrap-reverse';
918
+ if (wraps && props['alignContent'] == null)
919
+ props['alignContent'] = 'stretch';
920
+ }
921
+ /**
922
+ * A flex item that stretches across its line, with its size across the line `auto`, as CSS has
923
+ * it: as wide as a column, or as tall as a row, where Yoga takes the ratio over the stretch. In a
924
+ * column, a height given leaves the ratio nothing to work out. In a row, the line is as tall as
925
+ * the ratio makes the item from its width, at least, which is then the item's least height.
926
+ *
927
+ * ponytail: a width in points that does not grow, in a row, with any least or most height in
928
+ * points. One that grows, or is a share, keeps the ratio, as Yoga has it. A plain view only: an image keeps its picture's proportions.
929
+ */
930
+ function stretchedOverRatio(node, props) {
931
+ if (!isSet(props['aspectRatio']) || props['position'] === 'absolute')
932
+ return;
933
+ if (viewNameOf(node) !== DEFAULT_VIEW)
934
+ return;
935
+ const parent = layoutParent(node);
936
+ if (!parent)
937
+ return;
938
+ const container = containerOf(parent);
939
+ // Remembered on the container, so the item is merged again when its direction changes.
940
+ parent.fitContainer = container;
941
+ const [direction, align] = container.split(' ');
942
+ if (align !== 'stretch' || !stretchesAcross(props, direction === 'row'))
943
+ return;
944
+ if (direction === 'row')
945
+ ratioAsLeast(props);
946
+ else if (isSet(props['height']) && props['height'] !== 'auto')
947
+ delete props['aspectRatio'];
948
+ }
949
+ /** Whether an item stretches across its line: its own size and margins across it are `auto`. */
950
+ function stretchesAcross(props, row) {
951
+ if (!STRETCHES.has(props['alignSelf']))
952
+ return false;
953
+ const size = row ? 'height' : 'width';
954
+ if (isSet(props[size]) && props[size] !== 'auto')
955
+ return false;
956
+ const sides = row
957
+ ? MARGINS_ACROSS_ROW
958
+ : props['direction'] === 'rtl'
959
+ ? MARGINS_ACROSS_COLUMN_RTL
960
+ : MARGINS_ACROSS_COLUMN;
961
+ return sides.every((keys) => keys.map((key) => props[key]).find(isSet) !== 'auto');
962
+ }
963
+ /** The margins across a line, a side each, as Yoga reads them: the first set wins. */
964
+ const MARGINS_ACROSS_ROW = [
965
+ ['marginTop', 'marginVertical', 'margin'],
966
+ ['marginBottom', 'marginVertical', 'margin'],
967
+ ];
968
+ const MARGINS_ACROSS_COLUMN = [
969
+ ['marginStart', 'marginLeft', 'marginHorizontal', 'margin'],
970
+ ['marginEnd', 'marginRight', 'marginHorizontal', 'margin'],
971
+ ];
972
+ /** Right to left, where the start is the right side. */
973
+ const MARGINS_ACROSS_COLUMN_RTL = [
974
+ ['marginEnd', 'marginLeft', 'marginHorizontal', 'margin'],
975
+ ['marginStart', 'marginRight', 'marginHorizontal', 'margin'],
976
+ ];
977
+ /** In a row, the height a ratio gives a width in points, as the item's least height. */
978
+ function ratioAsLeast(props) {
979
+ const [width, ratio] = [props['width'], props['aspectRatio']];
980
+ if (typeof width !== 'number' || typeof ratio !== 'number' || ratio <= 0)
981
+ return;
982
+ const [least, most] = [props['minHeight'], props['maxHeight']];
983
+ const inPoints = (bound) => !isSet(bound) || typeof bound === 'number';
984
+ if (props['boxSizing'] === 'content-box' || !inPoints(least) || !inPoints(most))
985
+ return;
986
+ if (sizedAlong(props))
987
+ return;
988
+ props['minHeight'] = heldTo(width / ratio, least, most);
989
+ delete props['aspectRatio'];
990
+ }
991
+ /** Whether an item's width is not what it is along its line: it has a basis, or it grows. */
992
+ function sizedAlong(props) {
993
+ const basis = props['flexBasis'];
994
+ return (isSet(basis) && basis !== 'auto') || Number(props['flexGrow'] ?? props['flex'] ?? 0) > 0;
995
+ }
996
+ /** A height held to the least and most given, which CSS keeps: the least wins over the most. */
997
+ function heldTo(height, least, most) {
998
+ const under = typeof most === 'number' ? Math.min(height, most) : height;
999
+ return typeof least === 'number' ? Math.max(under, least) : under;
1000
+ }
857
1001
  /**
858
1002
  * Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
859
1003
  * for a size that is `auto`; Yoga takes it over one of two that are given, the height in a
@@ -879,6 +1023,20 @@ function colourless(props, side, flipped) {
879
1023
  * Each side a border has: its width, its colour, and the logical colour React Native takes for
880
1024
  * it too, read left to right and, `flipped`, right to left.
881
1025
  */
1026
+ const BORDER_DRAWN = /^border\w*(Width|Style)$/;
1027
+ const BORDER_COLOUR = /^border\w*Color$/;
1028
+ /**
1029
+ * Send a border's colours again with an update that gives it a width or a style: Android draws
1030
+ * the border black when that update does not carry its colour, though it was given one before.
1031
+ * A box made without a border and given one later, a file attachment that is done, has one.
1032
+ */
1033
+ function borderColoursAgain(payload, props) {
1034
+ if (!Object.keys(payload).some((key) => BORDER_DRAWN.test(key)))
1035
+ return;
1036
+ for (const key in props)
1037
+ if (BORDER_COLOUR.test(key) && !(key in payload))
1038
+ payload[key] = props[key];
1039
+ }
882
1040
  const BORDER_SIDES = [
883
1041
  ['Top', 'BlockStart', 'BlockStart'],
884
1042
  ['Right', 'End', 'Start'],
@@ -970,14 +1128,63 @@ function lineTall(props, run, fontScale) {
970
1128
  }
971
1129
  /** The points of padding and border above and below a view's content, where each is a length. */
972
1130
  function aroundALine(props) {
973
- const edge = (...keys) => {
1131
+ return around(props, 'Height');
1132
+ }
1133
+ /**
1134
+ * The props each side's padding and border are read from, across a height and across a width,
1135
+ * the first set winning, as React Native and then Yoga read them: an alias of React Native's over
1136
+ * Yoga's own, a logical side over the physical one, and the start on the left unless the box is
1137
+ * right to left. A block's start and end are under the top and bottom.
1138
+ */
1139
+ function sides(props, size) {
1140
+ if (size === 'Height') {
1141
+ const block = ['paddingBlock', 'paddingVertical', 'padding'];
1142
+ return [
1143
+ ['paddingTop', 'paddingBlockStart', ...block],
1144
+ ['borderTopWidth', 'borderWidth'],
1145
+ ['paddingBottom', 'paddingBlockEnd', ...block],
1146
+ ['borderBottomWidth', 'borderWidth'],
1147
+ ];
1148
+ }
1149
+ const rtl = props['direction'] === 'rtl';
1150
+ const inline = ['paddingInline', 'paddingHorizontal', 'padding'];
1151
+ return ['Left', 'Right'].flatMap((physical, at) => {
1152
+ const logical = (at === 0) !== rtl ? 'Start' : 'End';
1153
+ return [
1154
+ [`paddingInline${logical}`, `padding${logical}`, `padding${physical}`, ...inline],
1155
+ [`border${logical}Width`, `border${physical}Width`, 'borderWidth'],
1156
+ ];
1157
+ });
1158
+ }
1159
+ /** The points of padding and border across a view's height or width, where each is a length. */
1160
+ function around(props, size) {
1161
+ let total = 0;
1162
+ for (const keys of sides(props, size)) {
974
1163
  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'));
1164
+ if (typeof found === 'number')
1165
+ total += found;
1166
+ }
1167
+ return total;
1168
+ }
1169
+ /** The most and the least of each size, by name: asked of every node merged. */
1170
+ const MOST = { Height: 'maxHeight', Width: 'maxWidth' };
1171
+ const LEAST = { Height: 'minHeight', Width: 'minWidth' };
1172
+ /**
1173
+ * A most size under the least: under a `min-height`, or a border box's padding and border, which
1174
+ * CSS lets win (CSS 2.2, 10.7). React Native's Yoga holds the box to its most, and the box around
1175
+ * it to that, so the most is raised to the least. A most and a least in points: a share on either
1176
+ * side is left to Yoga.
1177
+ */
1178
+ function leastWins(props, size) {
1179
+ const most = props[MOST[size]];
1180
+ if (typeof most !== 'number')
1181
+ return;
1182
+ const written = props[LEAST[size]];
1183
+ let least = typeof written === 'number' ? written : 0;
1184
+ if (props['boxSizing'] !== 'content-box')
1185
+ least = Math.max(least, around(props, size));
1186
+ if (most < least)
1187
+ props[MOST[size]] = least;
981
1188
  }
982
1189
  /**
983
1190
  * Where a line may break: at a space, after a hyphen, and anywhere in a script written without
@@ -1096,8 +1303,18 @@ export function registerViewName(elementName, viewName, defaultProps, options) {
1096
1303
  LOOSE_TEXT[name] = options.looseText;
1097
1304
  else
1098
1305
  delete LOOSE_TEXT[name];
1306
+ if (options?.fontNames)
1307
+ FONT_NAMES[name] = options.fontNames;
1308
+ else
1309
+ delete FONT_NAMES[name];
1099
1310
  }
1100
1311
  }
1312
+ /** How each view that names fonts in its own props rewrites them. See `ViewNameOptions`. */
1313
+ const FONT_NAMES = {};
1314
+ /** Rewrites the fonts a view's own props name, for a view that says how. See `ViewNameOptions`. */
1315
+ function renameFonts(viewName, props, postScriptName) {
1316
+ FONT_NAMES[viewName]?.(props, postScriptName);
1317
+ }
1101
1318
  /** The element loose text in a view is drawn by, for the views that say. See `ViewNameOptions`. */
1102
1319
  const LOOSE_TEXT = {};
1103
1320
  /** The registered names that give way to a component's own host. See `ViewNameOptions`. */
@@ -1245,6 +1462,10 @@ const SCROLL_VIEWS = new Set(['ScrollView', 'AndroidHorizontalScrollView']);
1245
1462
  const HOISTS = {};
1246
1463
  /** The `ancestorName`s, so a commit asks only those nodes what to hoist into them. */
1247
1464
  const HOIST_TARGETS = new Set();
1465
+ /** The `ancestorName`s that commit only what is hoisted into them. See `HoistOptions.only`. */
1466
+ const HOIST_ONLY = new Set();
1467
+ /** Element name -> the ancestor names it never hoists past. See `HoistOptions.stopAt`. */
1468
+ const HOIST_STOPS = {};
1248
1469
  /**
1249
1470
  * Commit every `elementName` as a direct child of its nearest `ancestorName`, wherever the
1250
1471
  * template put it, e.g. `registerHoist('native-header', 'screen')`.
@@ -1263,6 +1484,12 @@ export function registerHoist(elementName, ancestorName, options = {}) {
1263
1484
  HOIST_TARGETS.add(ancestorName);
1264
1485
  if (options.standIn)
1265
1486
  STAND_INS[elementName] = options.standIn;
1487
+ if (options.only)
1488
+ HOIST_ONLY.add(ancestorName);
1489
+ if (options.stopAt)
1490
+ HOIST_STOPS[elementName] = new Set(options.stopAt);
1491
+ else
1492
+ delete HOIST_STOPS[elementName];
1266
1493
  }
1267
1494
  /** Element name -> the props it leaves behind in its ancestor. See `HoistOptions.standIn`. */
1268
1495
  const STAND_INS = {};
@@ -1295,10 +1522,12 @@ function paragraphText(node) {
1295
1522
  // Text written straight into a view is a paragraph of its own, and all of it.
1296
1523
  if (root === null || !isTextElement(root))
1297
1524
  return node.box ? trimEnds(node.text) : node.text;
1525
+ // A text element a view stands in for, which places what it holds: no paragraph runs past it.
1526
+ const inParagraph = (above) => isTextElement(above) && !aligningText(above);
1298
1527
  // The commonest paragraph by far: one run and nothing else, so it is both the first and last.
1299
- if (root.children.length === 1 && !isTextElement(root.parent))
1528
+ if (root.children.length === 1 && !inParagraph(root.parent))
1300
1529
  return trimEnds(node.text);
1301
- while (isTextElement(root.parent))
1530
+ while (inParagraph(root.parent))
1302
1531
  root = root.parent;
1303
1532
  const runs = [];
1304
1533
  const collect = (from) => {
@@ -1326,12 +1555,12 @@ function paragraphText(node) {
1326
1555
  function isTextElement(node) {
1327
1556
  if (node === null || node.kind !== 'element')
1328
1557
  return false;
1329
- if (node.name === 'text')
1558
+ if (node.tag === 'text')
1330
1559
  return true;
1331
1560
  // One lookup settles nearly every other element, which is a view.
1332
- if (registeredViewName(node.name) !== PARAGRAPH)
1561
+ if (registeredViewName(node.tag) !== PARAGRAPH)
1333
1562
  return false;
1334
- return !TEXT_ELEMENTS.has(node.name) || holdsOnlyText(node);
1563
+ return !TEXT_ELEMENTS.has(node.tag) || holdsOnlyText(node);
1335
1564
  }
1336
1565
  /** Whether a text element holds text and nothing else. One that holds nothing holds no text. */
1337
1566
  function holdsOnlyText(node) {
@@ -1351,25 +1580,48 @@ const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
1351
1580
  * alignment moves, so such an element commits as a view, and its text as the paragraph any view's
1352
1581
  * loose text is. Read from the node's own style on each reconcile, which a change to it brings
1353
1582
  * about, so nothing is kept to go stale. The run can be a text element of its own, a label in a
1354
- * span, which is then the item placed. A `<text>` is a paragraph whatever its style, and an
1355
- * element holding more than the one run is left as it was.
1583
+ * span, which is then the item placed. So is a column of text elements, each an item under the
1584
+ * last. A `<text>` is a paragraph whatever its style, and any other element holding more than
1585
+ * the one run is left as it was.
1356
1586
  */
1357
1587
  function aligningText(node) {
1358
- if (node.kind !== 'element' || node.name === 'text' || !TEXT_ELEMENTS.has(node.name)) {
1588
+ if (node.kind !== 'element' || node.tag === 'text' || !TEXT_ELEMENTS.has(node.tag)) {
1359
1589
  return false;
1360
1590
  }
1361
- if (node.children.length !== 1 || !oneRun(node.children[0]))
1591
+ const stacked = stacksRuns(node);
1592
+ if (!stacked && (node.children.length !== 1 || !oneRun(node.children[0])))
1362
1593
  return false;
1363
1594
  // Under a text element it is a span of that paragraph, unless that one is a view placing it.
1364
1595
  if (isTextElement(node.parent) && !aligningText(node.parent))
1365
1596
  return false;
1597
+ return placesByFlex(node, stacked);
1598
+ }
1599
+ /** Whether a flex container's own style places the run it holds, or stacks the runs it holds. */
1600
+ function placesByFlex(node, stacked) {
1366
1601
  if (ownLayout(node, 'display') !== 'flex')
1367
1602
  return false;
1603
+ // A column of them: each a line of its own, where one paragraph would run them on.
1604
+ if (stacked)
1605
+ return String(ownLayout(node, 'flexDirection') ?? 'column').startsWith('column');
1368
1606
  return (ALIGNS.has(ownLayout(node, 'alignItems')) ||
1369
1607
  ALIGNS.has(ownLayout(node, 'justifyContent')));
1370
1608
  }
1609
+ /**
1610
+ * Whether a text element holds two or more text elements and nothing else but whitespace and
1611
+ * the anchors control flow leaves: a label around a name and a hint, which a flex column has one under the other.
1612
+ */
1613
+ function stacksRuns(node) {
1614
+ let runs = 0;
1615
+ for (const child of node.children) {
1616
+ if (child.kind === 'element' && TEXT_ELEMENTS.has(child.tag))
1617
+ runs++;
1618
+ else if (child.kind === 'text' ? trimEnds(child.text) : child.kind !== 'anchor')
1619
+ return false;
1620
+ }
1621
+ return runs > 1;
1622
+ }
1371
1623
  /** One run of text to place: text itself, or a text element around some, a label in a span. */
1372
- const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.name));
1624
+ const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.tag));
1373
1625
  /** The view a node is committed as: `viewNameOf`, but for a text element that aligns its text. */
1374
1626
  const committedViewName = (node) => aligningText(node) ? DEFAULT_VIEW : viewNameOf(node);
1375
1627
  /**
@@ -1388,14 +1640,14 @@ export function viewNameOf(node) {
1388
1640
  // Committing a nested `<text>` as another Paragraph gives a paragraph inside a paragraph,
1389
1641
  // which lays out as a separate block instead of flowing inline.
1390
1642
  // A component of the app's own under a name a view set registered: its host is a plain view.
1391
- if (node.componentHost && !node.nativeView && YIELDING.has(node.name))
1643
+ if (node.componentHost && !node.nativeView && YIELDING.has(node.tag))
1392
1644
  return DEFAULT_VIEW;
1393
- const registered = registeredViewName(node.name);
1645
+ const registered = registeredViewName(node.tag);
1394
1646
  // Nearly every element: a view of some kind, by its name alone.
1395
1647
  if (registered !== PARAGRAPH)
1396
1648
  return registered ?? DEFAULT_VIEW;
1397
1649
  // One of HTML's text elements that holds something other than text: a view.
1398
- if (node.name !== 'text' && TEXT_ELEMENTS.has(node.name) && !holdsOnlyText(node)) {
1650
+ if (node.tag !== 'text' && TEXT_ELEMENTS.has(node.tag) && !holdsOnlyText(node)) {
1399
1651
  return DEFAULT_VIEW;
1400
1652
  }
1401
1653
  // The paragraph the engine makes for a view's loose text is under a view, whatever the name
@@ -1544,6 +1796,8 @@ const EASED_COLOURS = new Set([
1544
1796
  'borderLeftColor',
1545
1797
  ]);
1546
1798
  const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
1799
+ /** What a node that hands its background colour over has handed so far: nothing. */
1800
+ const UNREPORTED = Symbol('unreported');
1547
1801
  /** The style keys that are a view's background. */
1548
1802
  const PAINT_KEYS = [
1549
1803
  'backgroundColor',
@@ -1552,6 +1806,15 @@ const PAINT_KEYS = [
1552
1806
  'experimental_backgroundPosition',
1553
1807
  'experimental_backgroundRepeat',
1554
1808
  ];
1809
+ /** Note a prop written on a node that keeps `writes`: see there. */
1810
+ function noteWrite(node, key, value) {
1811
+ if (!node.writes)
1812
+ return;
1813
+ if (value === undefined || value === null)
1814
+ node.writes.delete(key);
1815
+ else
1816
+ node.writes.add(key);
1817
+ }
1555
1818
  /**
1556
1819
  * A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
1557
1820
  * attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
@@ -1718,6 +1981,17 @@ function reportHover(host) {
1718
1981
  hoverReported = true;
1719
1982
  console.warn('[angular-native] @defer (on hover) fires when a pointer enters the trigger, which takes a trackpad or a mouse (an iPad, or an Android device with one), or when the trigger takes focus. On a phone it waits for focus alone; add another trigger, such as on interaction or on viewport.');
1720
1983
  }
1984
+ /** A bound value the cascade works out from tokens: a `var()`, a `color-mix()`, a relative colour. */
1985
+ const BOUND_TOKEN = /^[ \t\n\r\f]*(?:var|color-mix)\(|^[ \t\n\r\f]*[a-z]+\([ \t\n\r\f]*from[ \t\n\r\f]/i;
1986
+ /**
1987
+ * A bound value kept as a declaration the cascade settles: a `var()`, a colour made of tokens, or
1988
+ * a relative length.
1989
+ */
1990
+ function boundFrom(prop, text, on) {
1991
+ return BOUND_TOKEN.test(text)
1992
+ ? boundDeclaration(prop, tokenFromValue(text), on)
1993
+ : boundRelativeLength(prop, text, on);
1994
+ }
1721
1995
  /** Whether a value is one of the engine's own nodes. */
1722
1996
  export function isEngineNode(value) {
1723
1997
  return value instanceof RetainedNode;
@@ -1777,17 +2051,18 @@ class RetainedNode {
1777
2051
  transitions;
1778
2052
  playing;
1779
2053
  scrolled;
2054
+ owned;
1780
2055
  kind;
1781
- name;
2056
+ tag;
1782
2057
  /** See `HostNode.host`. */
1783
2058
  host;
1784
- constructor(kind, name, host) {
2059
+ constructor(kind, tag, host) {
1785
2060
  this.kind = kind;
1786
- this.name = name;
2061
+ this.tag = tag;
1787
2062
  this.host = host;
1788
2063
  }
1789
2064
  get tagName() {
1790
- return this.name;
2065
+ return this.tag;
1791
2066
  }
1792
2067
  /** Angular's dev-mode assertion compares this against `Node.ELEMENT_NODE`. */
1793
2068
  get nodeType() {
@@ -2102,6 +2377,26 @@ function applyIntrinsicSize(props, size) {
2102
2377
  }
2103
2378
  }
2104
2379
  const now = () => globalThis.performance?.now?.() ?? Date.now();
2380
+ /** How many boxes `node` is under. */
2381
+ /**
2382
+ * How deep a node is, with the depth of every box on its way up kept in `known`: the boxes that
2383
+ * changed beneath are a node and its ancestors, so each is counted once, not once a box.
2384
+ */
2385
+ function depthOf(node, known) {
2386
+ const path = [];
2387
+ let depth = -1;
2388
+ for (let up = node; up; up = up.parent) {
2389
+ const found = known.get(up);
2390
+ if (found !== undefined) {
2391
+ depth = found;
2392
+ break;
2393
+ }
2394
+ path.push(up);
2395
+ }
2396
+ for (let i = path.length - 1; i >= 0; i--)
2397
+ known.set(path[i], ++depth);
2398
+ return depth;
2399
+ }
2105
2400
  function sameHandles(a, b) {
2106
2401
  return a.length === b.length && a.every((handle, i) => handle === b[i]);
2107
2402
  }
@@ -2124,6 +2419,11 @@ function couldBe(compound, node) {
2124
2419
  return (compound.is ?? []).every((alternatives) => alternatives.some((option) => couldBe(option, node)));
2125
2420
  }
2126
2421
  const FRAME_BUDGET_MS = 8;
2422
+ /** A solid brush with no colour in it, which stops react-native-svg. */
2423
+ const emptyBrush = (value) => typeof value === 'object' &&
2424
+ value !== null &&
2425
+ value.type === 0 &&
2426
+ value.payload == null;
2127
2427
  export class Engine {
2128
2428
  /** Scopes the CSS resolver's memo. Bumped once per commit. */
2129
2429
  styleEpoch = 0;
@@ -2214,7 +2514,7 @@ export class Engine {
2214
2514
  this.processColor = options.processColor ?? ((value) => value);
2215
2515
  this.resolveAssetSource = options.resolveAssetSource ?? ((value) => value);
2216
2516
  this.scrollDriver = options.nativeAnimated
2217
- ? new NativeScrollDriver(options.nativeAnimated)
2517
+ ? new NativeScrollDriver(options.nativeAnimated, (view, tag, keys) => this.touchedNatively(view, tag, keys))
2218
2518
  : null;
2219
2519
  // Eagerly, not on the first listener. Fabric holds exactly one event handler and
2220
2520
  // `ReactFabric` installs React's at module scope, so until we claim it every native event
@@ -2262,15 +2562,29 @@ export class Engine {
2262
2562
  */
2263
2563
  setActiveChain(from, active) {
2264
2564
  let changed = false;
2265
- for (let node = from; node; node = node.parent) {
2565
+ const set = (node) => {
2266
2566
  if (node.active === active)
2267
- continue;
2567
+ return;
2268
2568
  node.active = active;
2269
2569
  this.markActive(node);
2270
2570
  changed = true;
2571
+ };
2572
+ for (let node = from; node; node = node.parent) {
2573
+ set(node);
2574
+ if (active)
2575
+ this.activeNodes.add(node);
2576
+ }
2577
+ if (!active) {
2578
+ // Every node the press set, too: one the pressed node was moved or removed from mid-press
2579
+ // is no longer above it.
2580
+ for (const node of this.activeNodes)
2581
+ set(node);
2582
+ this.activeNodes.clear();
2271
2583
  }
2272
2584
  return changed;
2273
2585
  }
2586
+ /** The nodes a press has set `:active` on, to clear whatever happens to the tree meanwhile. */
2587
+ activeNodes = new Set();
2274
2588
  /** Whether any sheet styles a pressed element, and each compound that asks about one from another. */
2275
2589
  activeUse = {
2276
2590
  own: false,
@@ -2563,7 +2877,7 @@ export class Engine {
2563
2877
  if (sheet === undefined || node.sheet === sheet)
2564
2878
  return;
2565
2879
  node.sheet = sheet;
2566
- this.styles.noteSheet(sheet);
2880
+ this.styles.noteSheet(sheet, true);
2567
2881
  this.watchStructure(sheet);
2568
2882
  this.watchHas(sheet);
2569
2883
  this.watchActive(sheet);
@@ -2599,7 +2913,7 @@ export class Engine {
2599
2913
  createElement(name, sheet = null) {
2600
2914
  const node = new RetainedNode('element', name, this);
2601
2915
  node.sheet = sheet;
2602
- this.styles.noteSheet(sheet);
2916
+ this.styles.noteSheet(sheet, true);
2603
2917
  const html = HTML_ELEMENTS.get(name);
2604
2918
  if (html !== undefined) {
2605
2919
  if (html.role)
@@ -2629,8 +2943,10 @@ export class Engine {
2629
2943
  * element matched again, and one named as what a rule's element is inside, `.animating .close`
2630
2944
  * or `.close:is(.animating *)`, the elements under the node that such rules are for. What is
2631
2945
  * under any of those is styled again from the rules it matched, for what it inherits: a `dark`
2632
- * class on the root finds no rule again for an element no `dark` rule is for. Any other class
2633
- * has the node and everything under it matched again, as any other change to it does.
2946
+ * class on the root finds no rule again for an element no `dark` rule is for. One named in
2947
+ * `:has()`, or answering a test of the class attribute there, has the boxes above the node
2948
+ * asked again, and one named only in `:has(> )` the box right above it. Any other class has the node and everything under it matched again, as any
2949
+ * other change to it does.
2634
2950
  *
2635
2951
  * A sheet that names a class later restyles what it is for as it arrives, and a node not yet
2636
2952
  * committed is styled when it is, with the class as it is then.
@@ -2649,8 +2965,43 @@ export class Engine {
2649
2965
  return;
2650
2966
  if (reach.own)
2651
2967
  node.ownDirty = true;
2652
- if (this.styles.reachesUnder(reach))
2968
+ if (this.hasSheets && reach.beneath)
2969
+ this.markBeneath(node.parent);
2970
+ // `:has(> .on)`: only the box right above the node can answer it.
2971
+ else if (this.hasSheets && reach.parentHas)
2972
+ this.markBeneath(node.parent, node.parent?.parent);
2973
+ if (this.styles.reachesUnder(reach)) {
2974
+ this.classesAbove += 1;
2653
2975
  this.markUnder(node, reach);
2976
+ }
2977
+ }
2978
+ /**
2979
+ * How many class changes have restyled what is under an element. `markUnder` walks the tree as
2980
+ * it is, so an element out of it then is not reached: one that comes back after any is matched
2981
+ * again, with all that is under it.
2982
+ */
2983
+ classesAbove = 0;
2984
+ /**
2985
+ * The count when each element went out of the tree, for the ones still out. `detach` sets it
2986
+ * for a node that only moves too, so `markReturned` runs after it and clears that.
2987
+ */
2988
+ outSince = new WeakMap();
2989
+ /** Have matched again a node coming back into the tree that missed a class change above it. */
2990
+ markReturned(child) {
2991
+ const since = this.outSince.get(child);
2992
+ if (since === undefined)
2993
+ return;
2994
+ this.outSince.delete(child);
2995
+ if (since !== this.classesAbove)
2996
+ this.markAllUnder(child);
2997
+ }
2998
+ markAllUnder(node) {
2999
+ if (node.kind !== 'element')
3000
+ return;
3001
+ node.ownDirty = true;
3002
+ this.markPath(node);
3003
+ for (const child of node.children)
3004
+ this.markAllUnder(child);
2654
3005
  }
2655
3006
  /**
2656
3007
  * Have matched again each element under a node that one of `subjects`' rules could be for. One
@@ -2676,8 +3027,9 @@ export class Engine {
2676
3027
  if (node.hostSheet === sheet)
2677
3028
  return;
2678
3029
  node.hostSheet = sheet;
2679
- this.styles.noteSheet(sheet);
3030
+ this.styles.noteSheet(sheet, true);
2680
3031
  this.watchStructure(sheet);
3032
+ this.watchHas(sheet);
2681
3033
  this.watchActive(sheet);
2682
3034
  this.markProps(node);
2683
3035
  }
@@ -2687,6 +3039,7 @@ export class Engine {
2687
3039
  * the web's mount point has, unless the app's `:host` sizes it otherwise.
2688
3040
  */
2689
3041
  setDefaultStyle(node, style) {
3042
+ this.noteEasing(style);
2690
3043
  node.defaultStyle = style;
2691
3044
  // Under what the cascade comes to, and read by no selector: nothing is matched again.
2692
3045
  this.markProps(node, false);
@@ -2696,7 +3049,8 @@ export class Engine {
2696
3049
  * another order are a change too, to a selector that reads the class attribute, `[class^="a"]`.
2697
3050
  */
2698
3051
  setClasses(node, value) {
2699
- const names = value.split(/\s+/).filter(Boolean);
3052
+ // By HTML's spaces, which are CSS's: a vertical tab or a no-break space is part of a name.
3053
+ const names = value.split(CSS_SPACE).filter(Boolean);
2700
3054
  const before = node.classes;
2701
3055
  node.classes = names.length ? new Set(names) : null;
2702
3056
  // Each class that came or went: one in both changes nothing.
@@ -2719,6 +3073,7 @@ export class Engine {
2719
3073
  appendChild(parent, child) {
2720
3074
  this.markAdopted(child, parent);
2721
3075
  this.detach(child);
3076
+ this.markReturned(child);
2722
3077
  child.parent = parent;
2723
3078
  parent.children.push(child);
2724
3079
  if (child.dormantHoists)
@@ -2729,6 +3084,7 @@ export class Engine {
2729
3084
  insertBefore(parent, child, ref) {
2730
3085
  this.markAdopted(child, parent);
2731
3086
  this.detach(child);
3087
+ this.markReturned(child);
2732
3088
  child.parent = parent;
2733
3089
  const at = ref ? parent.children.indexOf(ref) : -1;
2734
3090
  if (at < 0)
@@ -2759,6 +3115,7 @@ export class Engine {
2759
3115
  return;
2760
3116
  target.children.splice(at, 1);
2761
3117
  child.parent = null;
3118
+ this.outSince.set(child, this.classesAbove);
2762
3119
  this.removedSinceCommit.add(child);
2763
3120
  this.markStructure(target, child, at);
2764
3121
  this.markTextContent(target, child);
@@ -2846,7 +3203,26 @@ export class Engine {
2846
3203
  }
2847
3204
  }
2848
3205
  if (this.focusedNode && this.topOf(this.focusedNode) !== this.root)
2849
- this.focusedNode = null;
3206
+ this.dropFocus();
3207
+ this.dropDetachedPress();
3208
+ }
3209
+ /** A pressed node out of the tree gets no release: native has no view to end the touch on. */
3210
+ dropDetachedPress() {
3211
+ const pressed = this.currentResponder;
3212
+ if (!pressed || this.topOf(pressed) === this.root)
3213
+ return;
3214
+ this.setActiveChain(pressed, false);
3215
+ this.currentResponder = null;
3216
+ }
3217
+ /**
3218
+ * Forget the focused node, which has left the tree without a blur: off the node as well as the
3219
+ * engine, or a `:focus` rule styles it again when it is put back.
3220
+ */
3221
+ dropFocus() {
3222
+ const node = this.focusedNode;
3223
+ node.focused = false;
3224
+ this.markProps(node);
3225
+ this.focusedNode = null;
2850
3226
  }
2851
3227
  /** Forget the committed views of each node removed since the last commit that is still out. */
2852
3228
  forgetDetached() {
@@ -2893,7 +3269,7 @@ export class Engine {
2893
3269
  node.kept = undefined;
2894
3270
  // A field popped with the keyboard up never sends its blur.
2895
3271
  if (this.focusedNode === node)
2896
- this.focusedNode = null;
3272
+ this.dropFocus();
2897
3273
  }
2898
3274
  parentNode(node) {
2899
3275
  return node.parent;
@@ -2904,7 +3280,46 @@ export class Engine {
2904
3280
  return null;
2905
3281
  return siblings[siblings.indexOf(node) + 1] ?? null;
2906
3282
  }
3283
+ /**
3284
+ * Whether any declaration the engine was given transitions or animates: until one does, a
3285
+ * node's style is committed as it is, with nothing asked of what eases it. See `eased`.
3286
+ */
3287
+ eases = false;
3288
+ noteEasing(declarations) {
3289
+ if (this.eases || !declarations)
3290
+ return;
3291
+ for (const key in declarations) {
3292
+ if (key.startsWith('$transition') || key.startsWith('$animation'))
3293
+ this.eases = true;
3294
+ }
3295
+ }
3296
+ /** A node's style as its transitions and animation have it: a declaration asks for one. */
3297
+ eased(node, style) {
3298
+ if (!this.eases)
3299
+ return style;
3300
+ return this.animated(node, this.transitioned(node, style));
3301
+ }
3302
+ /** Note a sheet any of whose rules transitions or animates: see `eases`. */
3303
+ noteSheetEasing(sheet) {
3304
+ for (const rule of this.eases ? [] : sheet.rules) {
3305
+ this.noteEasing(rule.declarations);
3306
+ this.noteEasing(rule.important);
3307
+ // One a token holds: `animation: var(--motion)`.
3308
+ for (const deferred of rule.deferred ?? []) {
3309
+ this.noteEasing(Object.fromEntries(deferred.props.map((key) => [key, true])));
3310
+ }
3311
+ }
3312
+ }
3313
+ /** Note a prop, or a style set as one, that transitions or animates: see `eases`. */
3314
+ noteEasingProp(key, value) {
3315
+ this.noteEasing({ [key]: value });
3316
+ if (key === 'style' || key === STYLE_OVERRIDE)
3317
+ this.noteEasing(flattenStyle(value, {}));
3318
+ }
2907
3319
  setProp(node, key, value) {
3320
+ if (!this.eases)
3321
+ this.noteEasingProp(key, value);
3322
+ noteWrite(node, key, value);
2908
3323
  // A prop written again is a binding's, and is committed as any other is.
2909
3324
  if (node.attributeOnly?.delete(key))
2910
3325
  this.markProps(node, false);
@@ -2930,7 +3345,9 @@ export class Engine {
2930
3345
  // And only a prop some selector names: `aria-valuenow` on every tick of a progress bar, a
2931
3346
  // field's text on every key, match nothing they did not.
2932
3347
  const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
2933
- this.markProps(node, read || heirs);
3348
+ // And restyles the elements after it only where a rule asks the prop of an element before
3349
+ // another, `[data-on] ~ .x`: a point of a chart set as a finger moves leaves those after it.
3350
+ this.markProps(node, read || heirs, this.styles.readsBeside(key));
2934
3351
  if (SIZES_A_RUN.has(key))
2935
3352
  this.markParagraph(node);
2936
3353
  }
@@ -2956,6 +3373,40 @@ export class Engine {
2956
3373
  this.markProps(node, false);
2957
3374
  this.markProps(child, false);
2958
3375
  }
3376
+ /**
3377
+ * Hand a node's background colour to `listener` instead of painting it: what its stylesheets,
3378
+ * tokens and inline style resolve `background-color` to, or undefined for none. Called after
3379
+ * each commit that changes it, the first one included. What a component needs whose colour is
3380
+ * drawn by a native view the node is not, from a prop of that view's, as a bottom sheet's is.
3381
+ * The function returned stops it, and the node paints its own background again.
3382
+ */
3383
+ takePaint(node, listener) {
3384
+ node.paintTaker = listener;
3385
+ node.paintTaken = UNREPORTED;
3386
+ this.markProps(node, false);
3387
+ return () => {
3388
+ if (node.paintTaker !== listener)
3389
+ return;
3390
+ node.paintTaker = undefined;
3391
+ this.paintReports.delete(node);
3392
+ this.markProps(node, false);
3393
+ };
3394
+ }
3395
+ /** Nodes whose background colour is to be handed over once the commit running ends. */
3396
+ paintReports = new Set();
3397
+ reportPaint() {
3398
+ const nodes = [...this.paintReports];
3399
+ this.paintReports.clear();
3400
+ // One listener that throws stops neither the rest nor what the commit does after them.
3401
+ for (const node of nodes) {
3402
+ try {
3403
+ node.paintTaker?.(node.paintTaken);
3404
+ }
3405
+ catch (error) {
3406
+ this.reportEventError(error, 'paint');
3407
+ }
3408
+ }
3409
+ }
2959
3410
  /**
2960
3411
  * Keep a prop the node already has as an attribute: on the node for a selector such as
2961
3412
  * `:host([variant='primary'])` to match, and left out of what is committed to the native view.
@@ -2998,27 +3449,43 @@ export class Engine {
2998
3449
  * has changed for the props diff to notice.
2999
3450
  */
3000
3451
  styleChanged(node) {
3001
- this.markProps(node, this.inlineReachesStyle(node));
3452
+ if (!this.eases)
3453
+ this.noteEasingProp('style', node.props['style']);
3454
+ // As `setProp` restyles for a new style object: a child that takes one of its values, an
3455
+ // `inherit`, follows the one changed in place too.
3456
+ this.markProps(node, this.inlineReachesStyle(node) || node.styleCache?.heirs === true);
3002
3457
  this.markParagraph(node);
3003
3458
  }
3004
3459
  /**
3005
- * A bound declaration, `[style.color]`, whose value may be a `var()`. One that is, is kept as a
3006
- * declaration the cascade settles with the node's tokens, as a rule's is, and true is answered:
3007
- * the text is no value to put in the node's style. Any other value forgets what was kept.
3460
+ * A bound declaration, `[style.color]`, whose value may be a `var()`, or a length in `em` or a
3461
+ * viewport unit. One that is, is kept as a declaration the cascade settles with the node's
3462
+ * tokens, font size and viewport, as a rule's is, and true is answered: the text is no value to
3463
+ * put in the node's style. A negative `line-height` is dropped, with a warning, and true is
3464
+ * answered too. Any other value forgets what was kept.
3008
3465
  */
3009
3466
  setBoundStyle(node, prop, value) {
3010
- const text = typeof value === 'string' && /^\s*var\(/i.test(value) ? value : undefined;
3011
- const bound = text === undefined ? null : boundDeclaration(prop, tokenFromValue(text), node.name);
3467
+ const text = typeof value === 'string' ? value : undefined;
3468
+ const refused = text !== undefined && this.refusedBound(node, prop, text);
3469
+ const bound = text === undefined || refused ? null : boundFrom(prop, text, node.tag);
3012
3470
  if (!bound && !node.boundByProp?.has(prop))
3013
- return false;
3471
+ return refused;
3014
3472
  const byProp = (node.boundByProp ??= new Map());
3015
3473
  if (bound)
3016
3474
  byProp.set(prop, bound);
3017
3475
  else
3018
3476
  byProp.delete(prop);
3019
3477
  node.boundStyle = byProp.size ? [...byProp.values()] : null;
3020
- this.markProps(node);
3021
- return bound !== null;
3478
+ // Settled by the cascade as the node is styled, and asked about by no selector: nothing after
3479
+ // the node matches otherwise for it.
3480
+ this.markProps(node, true, false);
3481
+ return bound !== null || refused;
3482
+ }
3483
+ /** Whether a bound value is one CSS has none of, which is dropped with a warning. */
3484
+ refusedBound(node, prop, text) {
3485
+ if (!isNegativeLineHeight(prop, text))
3486
+ return false;
3487
+ this.reportDroppedDeclaration('line-height', text, node.tag, 'it is negative');
3488
+ return true;
3022
3489
  }
3023
3490
  /**
3024
3491
  * Set a custom property on a node, `--tint` from `[style.--tint]`, or remove it with `null`.
@@ -3074,7 +3541,7 @@ export class Engine {
3074
3541
  return () => set.delete(fn);
3075
3542
  }
3076
3543
  // --- dirty marking ----------------------------------------------------
3077
- markProps(node, affectsStyle = true) {
3544
+ markProps(node, affectsStyle = true, beside = true) {
3078
3545
  node.propsDirty = true;
3079
3546
  // Classes, `nativeID` and anything else a selector can read live in props, so any of them
3080
3547
  // changing can change what this node matches, and through it what its whole subtree does.
@@ -3084,7 +3551,8 @@ export class Engine {
3084
3551
  node.styleDirty = true;
3085
3552
  // `.peer:focus ~ .label`: what a later sibling matches can hang on this node's classes and
3086
3553
  // state, though nothing about the sibling moved. Only sheets that ask about siblings pay.
3087
- if (this.structuralSheets)
3554
+ // Not for a change `beside` says no rule asks of an element before another.
3555
+ if (this.structuralSheets && beside)
3088
3556
  this.markLaterSiblings(node);
3089
3557
  if (this.hasSheets)
3090
3558
  this.markBeneath(node.parent);
@@ -3112,11 +3580,12 @@ export class Engine {
3112
3580
  }
3113
3581
  /**
3114
3582
  * Something at or beneath `from` changed: every node from there up may match a `:has()` rule
3115
- * it did not, or stop matching one. They are matched again rather than marked for restyling,
3116
- * which would restyle everything under them. Only sheets that use `:has()` pay.
3583
+ * it did not, or stop matching one, up to `upTo` and not it. They are matched again rather than
3584
+ * marked for restyling, which would restyle everything under them. Only sheets that use
3585
+ * `:has()` pay.
3117
3586
  */
3118
- markBeneath(from) {
3119
- for (let node = from; node; node = node.parent) {
3587
+ markBeneath(from, upTo = null) {
3588
+ for (let node = from; node && node !== upTo; node = node.parent) {
3120
3589
  node.hasDirty = true;
3121
3590
  if (this.hasReaders.size)
3122
3591
  this.changedBeneath.add(node);
@@ -3133,20 +3602,49 @@ export class Engine {
3133
3602
  * Have matched again what is under each box that changed beneath and that such a rule could
3134
3603
  * be asking: the nodes the rule is for, and no other. Once for a commit, however many changes
3135
3604
  * there were beneath the box: a table gains its rows one at a time.
3605
+ *
3606
+ * A rule asked of a box above is walked under the highest box that asks it, which reaches what
3607
+ * is under the boxes beneath that one as well: a rule `:has()` asks of any box is asked by every
3608
+ * box from the change up to the root, and walked once, from the root. The rules a box asks are
3609
+ * walked together, the tree beneath it once.
3136
3610
  */
3137
3611
  markUnderHas() {
3138
3612
  if (!this.changedBeneath.size)
3139
3613
  return;
3140
- for (const node of this.changedBeneath) {
3141
- for (const readers of this.hasReaders.values()) {
3142
- for (const { classes, subjects } of readers) {
3143
- if (classes.every((name) => node.classes?.has(name)))
3144
- this.markUnder(node, subjects);
3145
- }
3146
- }
3614
+ const readers = [...this.hasReaders.values()].flat();
3615
+ const depths = new Map();
3616
+ for (const node of this.changedBeneath)
3617
+ depthOf(node, depths);
3618
+ const boxes = [...this.changedBeneath].sort((a, b) => depths.get(a) - depths.get(b));
3619
+ // By box walked, the rules walked under it and under every box above it.
3620
+ const walked = new Map();
3621
+ for (const node of boxes) {
3622
+ let above;
3623
+ for (let up = node.parent; up && !above; up = up.parent)
3624
+ above = walked.get(up);
3625
+ const all = this.markUnderBox(node, readers, above);
3626
+ if (all)
3627
+ walked.set(node, all);
3147
3628
  }
3148
3629
  this.changedBeneath.clear();
3149
3630
  }
3631
+ /**
3632
+ * `markUnderHas` for one box: the rules it asks that no box above it was walked for, walked
3633
+ * together. What was walked under it and above it, which is `above` where it asks no more.
3634
+ */
3635
+ markUnderBox(node, readers, above) {
3636
+ const asked = [];
3637
+ for (const reader of readers) {
3638
+ if (above?.has(reader) || !reader.classes.every((name) => node.classes?.has(name)))
3639
+ continue;
3640
+ asked.push(reader);
3641
+ }
3642
+ if (!asked.length)
3643
+ return above;
3644
+ const subjects = asked.map((reader) => reader.subjects);
3645
+ this.markUnder(node, subjects.length === 1 ? subjects[0] : joinSubjects(subjects));
3646
+ return new Set([...(above ?? []), ...asked]);
3647
+ }
3150
3648
  /**
3151
3649
  * What `node` matches changed, which the elements after it can read: as many of them as a rule
3152
3650
  * steps along from one, every one for a `~`, and none where no sheet steps at all.
@@ -3332,6 +3830,7 @@ export class Engine {
3332
3830
  this.stats.slowCommits++;
3333
3831
  }
3334
3832
  this.flushTransitionEvents();
3833
+ this.reportPaint();
3335
3834
  this.playSized();
3336
3835
  this.scrollDriver?.afterCommit();
3337
3836
  // Read first: the commit that settles keyframes starts by clearing it.
@@ -3340,8 +3839,17 @@ export class Engine {
3340
3839
  this.settleKeyframes();
3341
3840
  if (facesAdded)
3342
3841
  this.rematchFonts();
3842
+ this.blurHidden();
3343
3843
  return true;
3344
3844
  }
3845
+ /** A field went out of view focused, in the commit running: see `undisplayed`. */
3846
+ blurAfterCommit = false;
3847
+ blurHidden() {
3848
+ if (!this.blurAfterCommit)
3849
+ return;
3850
+ this.blurAfterCommit = false;
3851
+ this.setFocused(null);
3852
+ }
3345
3853
  beforeCommit = null;
3346
3854
  announceCommit() {
3347
3855
  const run = this.beforeCommit;
@@ -3364,14 +3872,14 @@ export class Engine {
3364
3872
  /** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
3365
3873
  unseen = Infinity;
3366
3874
  /**
3367
- * A commit for what changed in the same turn as earlier commits, before a frame was drawn.
3368
- * `since` is how many commits there had been before the first of them (`stats.commits`).
3875
+ * A commit for what changed after earlier commits, before a frame has drawn them. `since` is
3876
+ * how many commits there had been before the first of them (`stats.commits`).
3369
3877
  *
3370
3878
  * A view those commits created has not been seen in the style they gave it, so what this one
3371
- * gives it is where it starts, and no transition runs toward it. That is what a browser
3372
- * does with a class added to an element in the turn that inserted it, and what `animate.enter`
3373
- * is built on: Angular adds the enter class after the render pass that created the element, and
3374
- * takes it off a frame later, which is the one change the transition is for.
3879
+ * gives it is where it starts, and no transition runs toward it. That is what a browser does
3880
+ * with a class added to an element before the frame after it was inserted, and what
3881
+ * `animate.enter` is built on: Angular adds the enter class after the render pass that created
3882
+ * the element, and takes it off a frame later, which is the one change the transition is for.
3375
3883
  */
3376
3884
  commitUnseen(since) {
3377
3885
  this.unseen = since;
@@ -3531,6 +4039,7 @@ export class Engine {
3531
4039
  const resolved = props['pointerEvents'];
3532
4040
  writeOwnProps(node.props, props, node.attributeOnly);
3533
4041
  withTextContent(node, viewName, props);
4042
+ truncatedAs(node, props);
3534
4043
  const cascaded = props['transform'];
3535
4044
  const inline = inlineOf(node);
3536
4045
  const style = boundTransform(node, Object.assign(props, inline), cascaded);
@@ -3547,6 +4056,8 @@ export class Engine {
3547
4056
  flattenStyle(node.props[STYLE_OVERRIDE], style);
3548
4057
  this.fontFaces.apply(style);
3549
4058
  holdLoadingFamily(node, style);
4059
+ // Once the face is matched: a family a view's own font names is the face its weight chose.
4060
+ renameFonts(viewName, style, this.postScriptName);
3550
4061
  if (viewName === PARAGRAPH) {
3551
4062
  alignText(style, this.directionOf(node, style));
3552
4063
  this.wholeWords(node, style);
@@ -3555,8 +4066,8 @@ export class Engine {
3555
4066
  if (this.fontsRefreshed)
3556
4067
  this.capForFonts(node, style);
3557
4068
  alignMultiline(viewName, style);
3558
- rowsTall(style, this.fontScale);
3559
- const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
4069
+ rowsTall(viewName, style, this.fontScale);
4070
+ const merged = composeTransform(node, this.eased(node, style));
3560
4071
  // On iOS: Android's field says the baseline of the line it is sized and centred by.
3561
4072
  // Still under the row that marked it: a row taken away is not committed again to unmark it.
3562
4073
  const row = node.onBaseline;
@@ -3566,6 +4077,10 @@ export class Engine {
3566
4077
  basisAsSize(node, merged);
3567
4078
  // After the basis, which is a size given where it is committed as one.
3568
4079
  ratioForAutoSize(merged);
4080
+ wrappedLinesStretch(merged);
4081
+ stretchedOverRatio(node, merged);
4082
+ leastWins(merged, 'Height');
4083
+ leastWins(merged, 'Width');
3569
4084
  noOutline(merged);
3570
4085
  this.borderInText(node, merged);
3571
4086
  // Last, on what is committed: an override or an animation can hide a box, or place it.
@@ -3577,6 +4092,7 @@ export class Engine {
3577
4092
  this.movePaint(node, merged);
3578
4093
  if (node.raised !== undefined)
3579
4094
  merged['zIndex'] ??= node.raised;
4095
+ keptForNative(node, merged);
3580
4096
  return merged;
3581
4097
  }
3582
4098
  /**
@@ -3677,6 +4193,11 @@ export class Engine {
3677
4193
  * its parent's: the cascade's, and over it the parent's inline style.
3678
4194
  */
3679
4195
  movePaint(node, merged) {
4196
+ if (node.paintTaker) {
4197
+ // Only the colour is handed over: a background image stays painted on the node.
4198
+ this.takenPaint(node, merged['backgroundColor']);
4199
+ delete merged['backgroundColor'];
4200
+ }
3680
4201
  if (node.paintsOn)
3681
4202
  for (const key of PAINT_KEYS)
3682
4203
  delete merged[key];
@@ -3688,6 +4209,13 @@ export class Engine {
3688
4209
  if (paint[key] !== undefined)
3689
4210
  merged[key] = paint[key];
3690
4211
  }
4212
+ /** Note the background colour a node that hands it over resolves to, where it changed. */
4213
+ takenPaint(node, color) {
4214
+ if (color === node.paintTaken)
4215
+ return;
4216
+ node.paintTaken = color;
4217
+ this.paintReports.add(node);
4218
+ }
3691
4219
  /**
3692
4220
  * A container whose direction or alignment changed since a child with a `fit-content` size
3693
4221
  * read it, or whose height stopped or started being one a percentage is taken of since a child
@@ -3770,6 +4298,7 @@ export class Engine {
3770
4298
  if (!sheet || this.knownSheets.has(sheet))
3771
4299
  return;
3772
4300
  this.knownSheets.add(sheet);
4301
+ this.noteSheetEasing(sheet);
3773
4302
  this.styles.noteSheet(sheet);
3774
4303
  this.sheetOrder.push(sheet);
3775
4304
  for (const name of Object.keys(sheet.keyframes ?? {})) {
@@ -3800,6 +4329,7 @@ export class Engine {
3800
4329
  this.knownSheets.delete(sheet);
3801
4330
  if (next && !this.knownSheets.has(next)) {
3802
4331
  this.knownSheets.add(next);
4332
+ this.noteSheetEasing(next);
3803
4333
  this.styles.noteSheet(next);
3804
4334
  this.sheetOrder[at] = next;
3805
4335
  }
@@ -3842,6 +4372,7 @@ export class Engine {
3842
4372
  }
3843
4373
  /** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
3844
4374
  fontFaces = new FontFaces();
4375
+ postScriptName = (name) => this.fontFaces.postScriptName(name);
3845
4376
  /** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
3846
4377
  facesAdded = false;
3847
4378
  /**
@@ -3865,7 +4396,10 @@ export class Engine {
3865
4396
  named(child.box);
3866
4397
  if (child.kind !== 'element')
3867
4398
  continue;
3868
- named(child);
4399
+ if (child.committed && FONT_NAMES[child.committed.viewName])
4400
+ this.markProps(child, false);
4401
+ else
4402
+ named(child);
3869
4403
  this.markFontText(child);
3870
4404
  }
3871
4405
  }
@@ -4081,8 +4615,9 @@ export class Engine {
4081
4615
  }
4082
4616
  /**
4083
4617
  * Native reached the end of an animation it was playing. The frame it ends at, or the resting
4084
- * style where it does not fill, is committed before the view is let go, so nothing shows of
4085
- * the frame it started at, which is what the view had been committed with.
4618
+ * style where it does not fill, is committed, and set on native where the animation set the
4619
+ * prop (see `setOwned`). The view is let go first, which puts nothing back on Fabric: it is
4620
+ * drawn where the animation left it until then.
4086
4621
  */
4087
4622
  endedNatively(node, running) {
4088
4623
  if (node.playing !== running || !running.native)
@@ -4091,9 +4626,62 @@ export class Engine {
4091
4626
  running.native = undefined;
4092
4627
  this.playedNatively.delete(node);
4093
4628
  const end = running.start + spec.duration * (spec.iterations ?? 1);
4629
+ native.stop();
4094
4630
  this.advancePlayer(node, Math.max(this.now(), end));
4095
4631
  this.commit();
4096
- native.stop();
4632
+ // The commit sets only what it changed, and the style the view rests at can be the frame it
4633
+ // was committed with as it played: native has drawn it elsewhere since.
4634
+ const committed = node.committed;
4635
+ if (node.owned && committed)
4636
+ this.setOwned(node, RESET_OWNED, committed.props);
4637
+ }
4638
+ /** A graph of native's was connected to a view: see `Touched`. */
4639
+ touchedNatively(node, tag, keys) {
4640
+ const taken = keys.filter((key) => key === 'opacity' || key === 'transform');
4641
+ if (!taken.length)
4642
+ return;
4643
+ if (node.owned?.tag !== tag)
4644
+ node.owned = { tag, keys: new Set() };
4645
+ for (const key of taken)
4646
+ node.owned.keys.add(key);
4647
+ }
4648
+ /**
4649
+ * Set on native a change of a prop a graph of native's has set on the view before, while no
4650
+ * graph of its holds it: the commit cannot change it (see `Touched`). The payload is committed
4651
+ * as well, so the shadow node is as the view is drawn. A transform native cannot take as
4652
+ * numbers, a matrix, is left committed alone.
4653
+ */
4654
+ setOwned(node, payload, props) {
4655
+ const owned = node.owned;
4656
+ const tag = node.committed?.tag;
4657
+ if (tag === undefined || owned.tag !== tag || !this.scrollDriver)
4658
+ return;
4659
+ const held = this.scrollDriver.held(node);
4660
+ const free = (key) => key in payload && owned.keys.has(key) && !held.has(key);
4661
+ const values = {};
4662
+ if (free('opacity')) {
4663
+ const opacity = props['opacity'];
4664
+ values.opacity = typeof opacity === 'number' ? opacity : 1;
4665
+ }
4666
+ if (free('transform')) {
4667
+ const transform = heldTransform(props['transform'], () => this.boxSize(node));
4668
+ if (transform)
4669
+ values.transform = transform;
4670
+ }
4671
+ if (values.opacity !== undefined || values.transform) {
4672
+ this.scrollDriver.set(node, tag, values);
4673
+ }
4674
+ }
4675
+ /** The size a view is laid out at, where it has been. */
4676
+ boxSize(node) {
4677
+ let size;
4678
+ this.measure(node, (frame) => (size = frame));
4679
+ return size;
4680
+ }
4681
+ /** Forget the props native set on a node's view, which is gone or made again. */
4682
+ disown(node) {
4683
+ node.owned = undefined;
4684
+ this.scrollDriver?.forget(node);
4097
4685
  }
4098
4686
  /** Take an animation back from native, which stops it there. The caller plays it on, or not. */
4099
4687
  stopNative(node, running) {
@@ -4369,8 +4957,9 @@ export class Engine {
4369
4957
  this.takeBackEased(node, state, props, spec, now);
4370
4958
  this.takeBackLaid(state, props, spec, now);
4371
4959
  const started = [];
4960
+ const before = spec && this.paintedBefore(node);
4372
4961
  for (const key of steppedKeys(props, spec, state)) {
4373
- if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
4962
+ if (step(state, key, props, spec?.[key] ?? spec?.['all'], now, before)) {
4374
4963
  this.running.add(node);
4375
4964
  started.push(key);
4376
4965
  this.emitTransition(node, 'topTransitionstart', key);
@@ -4381,6 +4970,29 @@ export class Engine {
4381
4970
  node.transitions = undefined;
4382
4971
  return props;
4383
4972
  }
4973
+ /**
4974
+ * What a view was last painted with, by property, for a transition that arrives with the change
4975
+ * it eases: CSS takes the transition from the style after the change and eases from the value
4976
+ * before it. Nothing for a view nobody has seen yet, which starts where it is. The individual
4977
+ * transforms are read back from the parts the painted transform was put together from.
4978
+ */
4979
+ paintedBefore(node) {
4980
+ const painted = node.committed?.props;
4981
+ // A view that paints its background on a child does not commit it as its own.
4982
+ if (!painted || node.paintsOn)
4983
+ return undefined;
4984
+ if (node.bornIn !== undefined && node.bornIn >= this.unseen)
4985
+ return undefined;
4986
+ const composed = node.composedTransform;
4987
+ if (!composed || painted['transform'] !== composed.value)
4988
+ return (key) => painted[key];
4989
+ return (key) => {
4990
+ if (key === 'transform')
4991
+ return composed.parts[3];
4992
+ const part = INDIVIDUAL_TRANSFORMS.indexOf(key);
4993
+ return part === -1 ? painted[key] : composed.parts[part];
4994
+ };
4995
+ }
4384
4996
  /** Nobody saw what the last commit gave a view it created, so this is where the view starts. */
4385
4997
  unseenTransitions(node) {
4386
4998
  // One with no commit it was born in has been seen, whatever was committed since.
@@ -4428,17 +5040,26 @@ export class Engine {
4428
5040
  /**
4429
5041
  * Commit where it ends each transition of a size or a place that just started, for native to
4430
5042
  * move the view there: a layout animation, which the commit asks for once it is known that
4431
- * nothing else in it moves. One still on its way is committed where it ends too.
5043
+ * nothing else in it moves. One still on its way is committed where it ends too. On a host
5044
+ * with no layout animation they are eased from JavaScript, but for one sent on again and again.
4432
5045
  *
4433
5046
  * A width eased from JavaScript is a commit a frame, and a commit lays the whole screen out.
4434
5047
  */
4435
5048
  layEased(node, state, started, props, now) {
4436
- if (!this.fabric.configureNextLayoutAnimation)
4437
- return;
5049
+ const native = this.fabric.configureNextLayoutAnimation !== undefined;
4438
5050
  for (const key of started) {
4439
5051
  const eased = state.get(key);
4440
5052
  if (!LAID_OUT.has(key) || eased.start > now || eased.duration <= 0)
4441
5053
  continue;
5054
+ if (this.sentOnAgain(node, key, eased, now)) {
5055
+ // Where it ends, at once: see `sentOnAgain`.
5056
+ Object.assign(eased, { current: eased.to, done: true });
5057
+ this.emitTransition(node, 'topTransitionend', key);
5058
+ props[key] = eased.to;
5059
+ continue;
5060
+ }
5061
+ if (!native)
5062
+ continue;
4442
5063
  eased.laid = true;
4443
5064
  this.laying.push({ node, key, eased });
4444
5065
  }
@@ -4446,6 +5067,24 @@ export class Engine {
4446
5067
  if (eased.laid && !eased.done)
4447
5068
  props[key] = eased.to;
4448
5069
  }
5070
+ /** When each size in transition was last started, and how many starts in a row came soon. */
5071
+ laidStarts = new WeakMap();
5072
+ /**
5073
+ * Whether a transition of a size is one sent on again and again, each before the last had
5074
+ * ended: a progress bar that steps every 50ms and eases each step over 150. Native moves the
5075
+ * view, and lays the screen out, every frame while a layout animation runs, and JavaScript
5076
+ * commits every frame while it eases one, so one started at every step is one that never
5077
+ * stops. The first, and one sent back before it ends, are played; from the second sent on in
5078
+ * a row, each is committed where it ends, and the steps are the motion. Played again once the
5079
+ * size has been left alone for as long as it lasts.
5080
+ */
5081
+ sentOnAgain(node, key, eased, now) {
5082
+ const starts = this.laidStarts.get(node) ?? this.laidStarts.set(node, new Map()).get(node);
5083
+ const last = starts.get(key);
5084
+ const soon = last && now - last.at < eased.duration ? last.soon + 1 : 0;
5085
+ starts.set(key, { at: now, soon });
5086
+ return soon >= 2;
5087
+ }
4449
5088
  /** The animations this commit lays out at their last frame, for native to move the views to. */
4450
5089
  laidPlayed = [];
4451
5090
  /**
@@ -4725,6 +5364,17 @@ export class Engine {
4725
5364
  this.lettingGo.add(node);
4726
5365
  return true;
4727
5366
  }
5367
+ /** Whether a node, or an element it is in, is `display: none`. */
5368
+ boxless(node) {
5369
+ for (let at = node; at; at = at.parent) {
5370
+ if (at.kind !== 'element')
5371
+ continue;
5372
+ this.styles.resolve(at, this.styleEpoch);
5373
+ if (ownLayout(at, 'display') === 'none')
5374
+ return true;
5375
+ }
5376
+ return false;
5377
+ }
4728
5378
  /** One `@keyframes` player, one frame on. */
4729
5379
  advancePlayer(node, now) {
4730
5380
  const running = node.playing;
@@ -4736,6 +5386,17 @@ export class Engine {
4736
5386
  this.playing.delete(node);
4737
5387
  return;
4738
5388
  }
5389
+ // One with no box, or in a box with none, paints nothing, and an endless one would keep the
5390
+ // frame loop going for good. It is cancelled, as a browser cancels it, and starts again when
5391
+ // the node is shown, as it is made again.
5392
+ if (this.boxless(node)) {
5393
+ this.cancelPlaying(node);
5394
+ this.playing.delete(node);
5395
+ node.playing = undefined;
5396
+ // A commit, for its `animationcancel` to be sent after, and to drop its last frame.
5397
+ this.markProps(node, false);
5398
+ return;
5399
+ }
4739
5400
  // Frames that say nothing paint nothing: a keyframe's one declaration refused at build time
4740
5401
  // leaves these. No frame of it is committed, and one that never ends has nothing left to do.
4741
5402
  const idle = running.tracks.size === 0;
@@ -4815,15 +5476,21 @@ export class Engine {
4815
5476
  }
4816
5477
  return this.animating;
4817
5478
  }
4818
- /** The host's conversions, applied to a copy. A removed prop travels as null and stays null. */
4819
- processed(props) {
5479
+ /**
5480
+ * The host's conversions, applied to a copy. A removed prop travels as `cleared` says, null for
5481
+ * nearly every one. On an update a colour dropped as unreadable is null too: JSI leaves out an
5482
+ * undefined key, and Fabric keeps what a clone leaves out.
5483
+ */
5484
+ processed(props, update) {
4820
5485
  const out = {};
4821
5486
  for (const key of Object.keys(props)) {
4822
5487
  const value = props[key];
4823
- if (value === null || value === undefined)
5488
+ if (value === null)
5489
+ out[key] = cleared(key);
5490
+ else if (value === undefined)
4824
5491
  out[key] = value;
4825
5492
  else if (isColorProp(key))
4826
- out[key] = this.painted(key, value);
5493
+ out[key] = this.colourProp(key, value, update);
4827
5494
  // A list of sources has already been resolved by whoever built it, as RN's resolver
4828
5495
  // also assumes: it passes any object through and only turns a number into one.
4829
5496
  else if (ASSET_PROPS.has(key) && !Array.isArray(value))
@@ -4985,7 +5652,7 @@ export class Engine {
4985
5652
  this.noteMoved(node, propsPayload, childHandles.length !== previous.childHandles.length, previous.props['text']);
4986
5653
  let handle = previous.handle;
4987
5654
  if (childrenChanged || propsPayload) {
4988
- handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
5655
+ handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload, true), childrenChanged ? childHandles : null);
4989
5656
  // A clone is the same view, so it keeps the tag it was created with, not the newest tag
4990
5657
  // handed out, which is some other node's.
4991
5658
  node.committed = { handle, tag: previous.tag, props, childHandles, viewName };
@@ -4996,6 +5663,10 @@ export class Engine {
4996
5663
  /** What a clone sends: the props that changed, and a text input's font once it registered. */
4997
5664
  propsPayload(node, previous, props) {
4998
5665
  const payload = diffProps(previous, props);
5666
+ if (payload)
5667
+ borderColoursAgain(payload, props);
5668
+ if (payload && node.owned)
5669
+ this.setOwned(node, payload, props);
4999
5670
  if (payload && this.fontsRefreshed && this.fontResends.delete(node)) {
5000
5671
  payload['fontFamily'] = props['fontFamily'];
5001
5672
  }
@@ -5024,10 +5695,17 @@ export class Engine {
5024
5695
  */
5025
5696
  reconcileChildren(node, viewName, style) {
5026
5697
  const context = style?.context;
5027
- if (HOIST_TARGETS.has(node.name))
5698
+ if (HOIST_ONLY.has(node.tag))
5699
+ return this.reconcileOnlyHoisted(node);
5700
+ if (HOIST_TARGETS.has(node.tag))
5028
5701
  return this.reconcileHoistTarget(node, context);
5029
5702
  const handles = [];
5030
5703
  for (const child of node.children) {
5704
+ const clean = this.cleanUnder(node, child, context);
5705
+ if (clean) {
5706
+ handles.push(clean);
5707
+ continue;
5708
+ }
5031
5709
  if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
5032
5710
  continue;
5033
5711
  }
@@ -5071,7 +5749,7 @@ export class Engine {
5071
5749
  return null;
5072
5750
  let box = text.box;
5073
5751
  // One made for another element, before the view was registered again, is done with.
5074
- if (box && box.name !== drawnBy) {
5752
+ if (box && box.tag !== drawnBy) {
5075
5753
  if (box.committed)
5076
5754
  this.forgetCommitted(box);
5077
5755
  box = undefined;
@@ -5083,6 +5761,9 @@ export class Engine {
5083
5761
  text.box = box;
5084
5762
  }
5085
5763
  box.parent = parent;
5764
+ // Merged before the view is, so the view's truncation is read here: see `truncatedAs`.
5765
+ if (truncationMoved(box, parent))
5766
+ box.propsDirty = true;
5086
5767
  // The text is not under the box in the tree, so a change to it never marked the box.
5087
5768
  if (!this.isClean(text)) {
5088
5769
  box.subtreeDirty = true;
@@ -5143,10 +5824,12 @@ export class Engine {
5143
5824
  return false;
5144
5825
  if (child.committed) {
5145
5826
  this.forgetCommitted(child);
5146
- // A field in it has no view to send its blur from.
5827
+ // A field in it has no view to send its blur from. Blurred once this commit is done: a
5828
+ // `:focus` rule that hid it then no longer holds, and the commit that follows shows it.
5147
5829
  if (isWithin(this.focusedNode, child))
5148
- this.setFocused(null);
5830
+ this.blurAfterCommit = true;
5149
5831
  }
5832
+ settlePaths(child);
5150
5833
  return true;
5151
5834
  }
5152
5835
  /**
@@ -5177,13 +5860,13 @@ export class Engine {
5177
5860
  // which RNS keeps clear by never letting the header config be the first.
5178
5861
  const direct = [];
5179
5862
  for (const child of node.children) {
5180
- if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
5863
+ const taken = this.takenByTarget(node, child, context);
5864
+ if (taken === null)
5181
5865
  continue;
5182
- }
5183
- if (this.hoisted.has(child))
5866
+ if (taken === 'direct')
5184
5867
  direct.push(child);
5185
5868
  else
5186
- handles.push(this.reconcileUnder(node, child, context));
5869
+ handles.push(taken);
5187
5870
  const moved = hoisted?.get(child);
5188
5871
  for (const written of moved ?? []) {
5189
5872
  if (!this.undisplayedUnder(node, written))
@@ -5196,6 +5879,33 @@ export class Engine {
5196
5879
  this.standIn(node, landed, handles);
5197
5880
  return handles;
5198
5881
  }
5882
+ /**
5883
+ * `reconcileHoistTarget` for a node that commits only what is hoisted into it
5884
+ * (`HoistOptions.only`): those nodes, in the order they were written, and none of its own.
5885
+ */
5886
+ reconcileOnlyHoisted(node) {
5887
+ const handles = [];
5888
+ const hoisted = this.hoistedInto(node);
5889
+ const landed = new Set();
5890
+ for (const child of node.children) {
5891
+ if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
5892
+ continue;
5893
+ }
5894
+ if (this.hoisted.has(child)) {
5895
+ handles.push(this.land(node, child, landed));
5896
+ continue;
5897
+ }
5898
+ for (const written of hoisted?.get(child) ?? []) {
5899
+ if (!this.undisplayedUnder(node, written))
5900
+ handles.push(this.land(node, written, landed));
5901
+ }
5902
+ // Not committed, so the marks a change left on the way down to it are cleared here.
5903
+ settlePaths(child);
5904
+ }
5905
+ if (node.kept)
5906
+ this.standIn(node, landed, handles);
5907
+ return handles;
5908
+ }
5199
5909
  /**
5200
5910
  * A child's handle as a child of `parent`, created again when it was committed under another.
5201
5911
  *
@@ -5228,8 +5938,19 @@ export class Engine {
5228
5938
  forgetCommitted(node) {
5229
5939
  node.committed = null;
5230
5940
  node.committedUnder = null;
5941
+ // Another view, with another tag, which no graph has set anything on.
5942
+ if (node.owned)
5943
+ this.disown(node);
5231
5944
  node.styleCommitted = null;
5232
5945
  node.kept = undefined;
5946
+ // A scroll-driven animation is bound to native views by their tags: its own, and its scroll
5947
+ // view's. Made again, each has another, so what drives it starts again with the new ones.
5948
+ if (node.scrolled?.drive)
5949
+ this.stopScrolled(node);
5950
+ for (const follower of this.scrollTimelines.get(node) ?? []) {
5951
+ this.stopScrolled(follower);
5952
+ this.markProps(follower, false);
5953
+ }
5233
5954
  if (node.box?.committed)
5234
5955
  this.forgetCommitted(node.box);
5235
5956
  for (const child of node.children) {
@@ -5246,9 +5967,9 @@ export class Engine {
5246
5967
  * keeps for its name when it has one (`HoistOptions.standIn`), and keep the view it commits as.
5247
5968
  */
5248
5969
  land(target, child, landed) {
5249
- if (!STAND_INS[child.name])
5970
+ if (!STAND_INS[child.tag])
5250
5971
  return this.reconcileUnder(target, child);
5251
- const kept = target.kept?.get(child.name);
5972
+ const kept = target.kept?.get(child.tag);
5252
5973
  if (kept && kept.node !== child && child.committed === null && !isWithin(kept.node, target)) {
5253
5974
  // A node taken out without being destroyed would otherwise come back holding this tag too.
5254
5975
  if (kept.node)
@@ -5258,8 +5979,8 @@ export class Engine {
5258
5979
  }
5259
5980
  const handle = this.reconcile(child);
5260
5981
  child.committedUnder = target;
5261
- (target.kept ??= new Map()).set(child.name, { node: child, committed: child.committed });
5262
- landed.add(child.name);
5982
+ (target.kept ??= new Map()).set(child.tag, { node: child, committed: child.committed });
5983
+ landed.add(child.tag);
5263
5984
  return handle;
5264
5985
  }
5265
5986
  /**
@@ -5276,13 +5997,40 @@ export class Engine {
5276
5997
  ...STAND_INS[name],
5277
5998
  };
5278
5999
  const payload = diffProps(kept.committed.props, props);
5279
- const handle = this.clone(kept.committed.handle, payload && this.processed(payload), []);
6000
+ const handle = this.clone(kept.committed.handle, payload && this.processed(payload, true), []);
5280
6001
  kept.committed = { ...kept.committed, handle, props, childHandles: [] };
5281
6002
  kept.node = null;
5282
6003
  }
5283
6004
  handles.push(kept.committed.handle);
5284
6005
  }
5285
6006
  }
6007
+ /**
6008
+ * What a hoist target takes of a child written in it: the child's handle, `direct` for one
6009
+ * hoisted into it, which goes after its siblings, or null for none.
6010
+ */
6011
+ takenByTarget(node, child, context) {
6012
+ if (this.hoisted.has(child)) {
6013
+ if (this.committedElsewhere(child) || this.withheld(child))
6014
+ return null;
6015
+ return 'direct';
6016
+ }
6017
+ const clean = this.cleanUnder(node, child, context);
6018
+ if (clean)
6019
+ return clean;
6020
+ if (child.kind === 'anchor' || this.withheld(child))
6021
+ return null;
6022
+ return this.reconcileUnder(node, child, context);
6023
+ }
6024
+ /**
6025
+ * A clean element's handle under `parent`, before anything else is asked of it: one committed
6026
+ * there and unchanged since was displayed and in its place, and is still. The siblings of what
6027
+ * changed are most of what a commit steps over: a list one row of which ticks.
6028
+ */
6029
+ cleanUnder(parent, child, context) {
6030
+ if (child.kind !== 'element' || child.committedUnder !== parent)
6031
+ return null;
6032
+ return this.stillCommitted(child, context);
6033
+ }
5286
6034
  /** A clean child's committed handle, when its style still holds under its parent's. */
5287
6035
  stillCommitted(child, context) {
5288
6036
  const committed = child.committed;
@@ -5305,11 +6053,13 @@ export class Engine {
5305
6053
  hoistedInto(node) {
5306
6054
  let found = null;
5307
6055
  for (const candidate of this.hoisted) {
5308
- if (candidate.parent === node || HOISTS[candidate.name] !== node.name)
6056
+ if (candidate.parent === node || HOISTS[candidate.tag] !== node.tag)
5309
6057
  continue;
5310
6058
  let written = candidate;
5311
6059
  let above = candidate.parent;
5312
- while (above && above !== node && above.name !== node.name) {
6060
+ while (above && above !== node && above.tag !== node.tag) {
6061
+ if (HOIST_STOPS[candidate.tag]?.has(above.tag))
6062
+ break;
5313
6063
  written = above;
5314
6064
  above = above.parent;
5315
6065
  }
@@ -5326,12 +6076,14 @@ export class Engine {
5326
6076
  }
5327
6077
  /** Whether a hoisted node commits further up than the parent it was written in. */
5328
6078
  hoistsAbove(node) {
5329
- const target = HOISTS[node.name];
5330
- if (node.parent?.name === target)
6079
+ const target = HOISTS[node.tag];
6080
+ if (node.parent?.tag === target)
5331
6081
  return false;
5332
- for (let above = node.parent?.parent; above; above = above.parent) {
5333
- if (above.name === target)
6082
+ for (let above = node.parent; above; above = above.parent) {
6083
+ if (above.tag === target)
5334
6084
  return true;
6085
+ if (HOIST_STOPS[node.tag]?.has(above.tag))
6086
+ return false;
5335
6087
  }
5336
6088
  return false;
5337
6089
  }
@@ -5343,7 +6095,7 @@ export class Engine {
5343
6095
  const tag = nextTag();
5344
6096
  this.stats.createdNodes++;
5345
6097
  // instanceHandle is the retained node itself: it is what Fabric hands back on events.
5346
- const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props), node);
6098
+ const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props, false), node);
5347
6099
  for (const child of childHandles)
5348
6100
  this.fabric.appendChild(handle, child);
5349
6101
  node.committed = { handle, tag, props, childHandles, viewName };
@@ -5360,13 +6112,13 @@ export class Engine {
5360
6112
  // The paragraph the engine wraps loose text in is its own: no template wrote it.
5361
6113
  if (node.kind !== 'element' || node.claimed || node.anonymous)
5362
6114
  return;
5363
- if (!PRIMITIVE_NAMES.has(node.name))
6115
+ if (!PRIMITIVE_NAMES.has(node.tag))
5364
6116
  return;
5365
- if (this.reported.has(node.name))
6117
+ if (this.reported.has(node.tag))
5366
6118
  return;
5367
- this.reported.add(node.name);
5368
- console.error(`[angular-native] <${node.name}> is used in a template that does not import ` +
5369
- `${className(node.name)}. Add it to the component's \`imports\` from ` +
6119
+ this.reported.add(node.tag);
6120
+ console.error(`[angular-native] <${node.tag}> is used in a template that does not import ` +
6121
+ `${className(node.tag)}. Add it to the component's \`imports\` from ` +
5370
6122
  `'@ng-native/components'; without it the element has none of the component's inputs ` +
5371
6123
  `or behavior, so a prop written on it stays the text it was written as.`);
5372
6124
  }
@@ -5389,6 +6141,19 @@ export class Engine {
5389
6141
  ` scope defines, so the declaration is dropped, as a browser drops it. Define ${name} on` +
5390
6142
  ` :root or an ancestor, or give the var() a fallback.`);
5391
6143
  }
6144
+ droppedDeclarations = new Set();
6145
+ /**
6146
+ * A declaration in a style, written or bound, that is no value its property takes, so it is
6147
+ * dropped as a browser drops it. Said once per element name and declaration, in development.
6148
+ */
6149
+ reportDroppedDeclaration(prop, value, on, why) {
6150
+ const report = `${on} ${prop}: ${value}`;
6151
+ if (!this.dev || this.droppedDeclarations.has(report))
6152
+ return;
6153
+ this.droppedDeclarations.add(report);
6154
+ console.warn(`[angular-native] ${prop}: ${value}, in a style on <${on}>, is no value ${prop} takes:` +
6155
+ ` ${why}. The declaration is dropped, as a browser drops it.`);
6156
+ }
5392
6157
  unreadDisplays = new Set();
5393
6158
  /**
5394
6159
  * Said once per token and value: the compiler refuses `display: grid` written out, but a token
@@ -5404,23 +6169,23 @@ export class Engine {
5404
6169
  ` and contents, and reads block, inline, inline-block, flow-root and inline-flex as flex.`);
5405
6170
  }
5406
6171
  checkProps(node) {
5407
- if (!node.claimed || !PRIMITIVE_NAMES.has(node.name))
6172
+ if (!node.claimed || !PRIMITIVE_NAMES.has(node.tag))
5408
6173
  return;
5409
- const declared = DECLARED_PROPS.get(node.name);
6174
+ const declared = DECLARED_PROPS.get(node.tag);
5410
6175
  if (!declared)
5411
6176
  return;
5412
6177
  for (const key of Object.keys(node.props)) {
5413
6178
  if (this.isDeclared(declared, key, node.props[key]))
5414
6179
  continue;
5415
- const report = `${node.name} ${key}`;
6180
+ const report = `${node.tag} ${key}`;
5416
6181
  if (this.reported.has(report))
5417
6182
  continue;
5418
6183
  this.reported.add(report);
5419
6184
  const meant = [...declared].find((name) => name.toLowerCase() === key.toLowerCase());
5420
- console.warn(`[angular-native] <${node.name}> has no prop '${key}', so native ignores it.` +
6185
+ console.warn(`[angular-native] <${node.tag}> has no prop '${key}', so native ignores it.` +
5421
6186
  (meant ? ` Did you mean '${meant}'?` : '') +
5422
- ` Check the spelling against the ${className(node.name)} page, or declare it with` +
5423
- ` declareNativeProps('${node.name}', ['${key}']) if the native view does read it.`);
6187
+ ` Check the spelling against the ${className(node.tag)} page, or declare it with` +
6188
+ ` declareNativeProps('${node.tag}', ['${key}']) if the native view does read it.`);
5424
6189
  }
5425
6190
  }
5426
6191
  isDeclared(declared, key, value) {
@@ -5460,12 +6225,12 @@ export class Engine {
5460
6225
  * the trade for not holding a registry per component, and the typo is the case that matters.
5461
6226
  */
5462
6227
  checkKnown(node) {
5463
- if (node.kind !== 'element' || node.componentHost || node.name in VIEW_NAMES)
6228
+ if (node.kind !== 'element' || node.componentHost || node.tag in VIEW_NAMES)
5464
6229
  return;
5465
- if (this.declaredElements.has(node.name) || this.reported.has(node.name))
6230
+ if (this.declaredElements.has(node.tag) || this.reported.has(node.tag))
5466
6231
  return;
5467
- this.reported.add(node.name);
5468
- console.error(`[angular-native] <${node.name}> is not a known element: no component, directive or ` +
6232
+ this.reported.add(node.tag);
6233
+ console.error(`[angular-native] <${node.tag}> is not a known element: no component, directive or ` +
5469
6234
  `registered native view claims it, so it renders as an empty view. Check the spelling, ` +
5470
6235
  `or import the component that owns it.`);
5471
6236
  }
@@ -5605,7 +6370,7 @@ export class Engine {
5605
6370
  */
5606
6371
  get focused() {
5607
6372
  if (this.focusedNode && !this.isAttached(this.focusedNode))
5608
- this.focusedNode = null;
6373
+ this.dropFocus();
5609
6374
  return this.focusedNode;
5610
6375
  }
5611
6376
  /** Whether a node is still part of the tree, rather than in a subtree that was removed. */
@@ -5622,17 +6387,29 @@ export class Engine {
5622
6387
  resolveAsset(value) {
5623
6388
  return this.resolveAssetSource(value);
5624
6389
  }
6390
+ /** A colour prop as `processed` sends it: one dropped as unreadable is null on an update. */
6391
+ colourProp(key, value, update) {
6392
+ const painted = this.painted(key, value);
6393
+ return painted === undefined && update ? null : painted;
6394
+ }
5625
6395
  /**
5626
6396
  * A colour prop as native takes it. A shape's `fill` or `stroke` the cascade settled from a
5627
- * token is the solid brush react-native-svg takes, and `none` no paint at all.
6397
+ * token is the solid brush react-native-svg takes, `none` no paint at all, and one the host
6398
+ * reads no colour in no paint of the shape's own.
5628
6399
  */
5629
6400
  painted(key, value) {
5630
6401
  if (!BRUSH_PROPS.has(key))
5631
6402
  return this.color(value);
5632
- // Already a brush, as the icon wrote it: only one the cascade settled is still text.
6403
+ // Already a brush, as the icon wrote it: only one the cascade settled is still text. An
6404
+ // icon's solid brush holds whatever the host read, which is nothing for an unreadable colour.
5633
6405
  if (typeof value !== 'string')
5634
- return value;
5635
- return value === 'none' ? null : { type: 0, payload: this.color(value) };
6406
+ return emptyBrush(value) ? undefined : value;
6407
+ if (value === 'none')
6408
+ return null;
6409
+ // A colour the host cannot read is dropped, as a browser drops it: a solid brush with no
6410
+ // colour in it stops the app.
6411
+ const payload = this.color(value);
6412
+ return payload == null ? undefined : { type: 0, payload };
5636
6413
  }
5637
6414
  /**
5638
6415
  * The host's `processColor`, and the one place a platform colour becomes one.
@@ -5973,7 +6750,7 @@ export class Engine {
5973
6750
  for (let n = target; n; n = DIRECT_EVENTS.has(topLevelType) ? null : n.parent) {
5974
6751
  matched += n.listeners?.get(topLevelType)?.size ?? 0;
5975
6752
  }
5976
- console.log('[event]', topLevelType, target?.name ?? '(no handle)', matched);
6753
+ console.log('[event]', topLevelType, target?.tag ?? '(no handle)', matched);
5977
6754
  }
5978
6755
  /**
5979
6756
  * `:focus` follows the native events rather than being inferred from a tap, so it is right for a
@@ -6032,4 +6809,17 @@ export class Engine {
6032
6809
  this.removeChild(child.parent, child);
6033
6810
  }
6034
6811
  }
6812
+ /**
6813
+ * Clear the marks that lead a commit down to a change, under a node no commit goes into: one with
6814
+ * no box. Left set, a later change under it stops climbing at them, as it stops at any mark already
6815
+ * there, and never reaches the root: nothing is committed, and an ancestor's `:has()` reading into
6816
+ * it is not asked again. What is under it has no views, so it is reconciled whole when it is shown.
6817
+ */
6818
+ function settlePaths(node) {
6819
+ if (!node.subtreeDirty)
6820
+ return;
6821
+ node.subtreeDirty = false;
6822
+ for (const child of node.children)
6823
+ settlePaths(child);
6824
+ }
6035
6825
  //# sourceMappingURL=engine.js.map