@ng-native/fabric 0.17.1 → 0.18.1

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/engine.js CHANGED
@@ -7,19 +7,35 @@
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, boundRelativeLength, CSS_SPACE, isNegativeLineHeight, faded, fills, inlineInherited, setsInherited, attributeOf, handsDownPaint, StyleResolver, underOwnShorthands, usesActive, elementsAtEnd, hasAbove, couldAsk, hasBeside, forgetHasAnswers, keepHasAnswers, joinSubjects, placeReadElsewhere, siblingReach, isGenerated, noteGeneratedBoxes, } from "./css.js";
10
+ import { boundDeclaration, boundRelativeLength, CSS_SPACE, isNegativeLineHeight, faded, fills, inlineInherited, setsInherited, attributeOf, handsDownPaint, StyleResolver, underOwnShorthands, usesActive, elementsAtEnd, hasAbove, couldAsk, hasBeside, forgetHasAnswers, keepHasAnswers, joinSubjects, placeReadElsewhere, siblingReach, isGenerated, noteGeneratedBoxes, settleUnsidedBorder, } from "./css.js";
11
11
  import { applyAria } from "./aria-props.js";
12
12
  import { STYLED_ELEMENTS } from "./element-styles.js";
13
- import { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
13
+ import { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, parseColor, step, steppedKeys, tracksOf, } from "./transition.js";
14
14
  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, pinnedRange, scrollFeed, } from "./native-drive.js";
18
+ import { amount } from "./scroll-animation.js";
19
+ import { NativePainter } from "./native-paint.js";
18
20
  import { clockChannels, colourChannels, firstFrame, heldTransform, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
19
21
  import { layOutGrid } from "./grid.js";
20
22
  import { isBlockContainer, placeInFlow, planFlow, } from "./flow.js";
21
23
  import { FontFaces } from "./font-faces.js";
22
24
  import { premultipliedStops } from "./premultiplied-stops.js";
25
+ /** The lowest node both are in, either of them included, or nothing for two separate trees. */
26
+ function commonAncestor(a, b) {
27
+ // Most often `b` is inside `a`, which a walk up from it finds with no set made each move.
28
+ for (let node = b; node; node = node.parent)
29
+ if (node === a)
30
+ return a;
31
+ const above = new Set();
32
+ for (let node = a; node; node = node.parent)
33
+ above.add(node);
34
+ for (let node = b; node; node = node.parent)
35
+ if (above.has(node))
36
+ return node;
37
+ return null;
38
+ }
23
39
  /**
24
40
  * The animation a node's style asks for, with a play state from a rule of its own applied. Both
25
41
  * are instructions for this engine and nothing native has heard of them, so they come out of the
@@ -87,6 +103,118 @@ const SPEC_FIELDS = [
87
103
  'direction',
88
104
  'timeline',
89
105
  ];
106
+ /** No key: what a view holds with no graph of the driver's connected to it. */
107
+ const NO_KEYS = new Set();
108
+ /** How soon after one change of paint the next makes a run of them: see `paintedAgain`. */
109
+ const PAINT_RUN_MS = 1000;
110
+ /** How long a view native paints is left alone before its shadow node is brought up to date. */
111
+ const PAINT_QUIET_MS = 500;
112
+ const SHARED_SIDE = { translateX: 2, translateY: 3 };
113
+ /**
114
+ * A transform as native's transform node takes it, each entry's amount a number, and a move by a
115
+ * share of the box the points it is of the box `boxOf` answers. Null for one it cannot take: a
116
+ * matrix, or a share of a box with no size yet.
117
+ */
118
+ function paintedTransform(value, boxOf) {
119
+ const list = typeof value === 'string' ? transformList(value) : value;
120
+ if (!Array.isArray(list))
121
+ return null;
122
+ const out = [];
123
+ for (const entry of list) {
124
+ const pairs = Object.entries((entry ?? {}));
125
+ if (pairs.length !== 1)
126
+ return null;
127
+ const [property, raw] = pairs[0];
128
+ const flat = property === 'matrix' ? flatMatrix(raw) : null;
129
+ if (flat) {
130
+ out.push(...flat);
131
+ continue;
132
+ }
133
+ const points = amount(property, raw) ?? shareInPoints(property, raw, boxOf);
134
+ if (points === null || !Number.isFinite(points))
135
+ return null;
136
+ out.push({ property, value: points });
137
+ }
138
+ return out;
139
+ }
140
+ /** Where each number of a flat matrix is in React Native's sixteen, by column. */
141
+ const FLAT = { a: 0, b: 1, c: 4, d: 5, e: 12, f: 13 };
142
+ /** What the rest of a flat matrix's sixteen are. */
143
+ const FLAT_REST = [
144
+ [2, 0],
145
+ [3, 0],
146
+ [6, 0],
147
+ [7, 0],
148
+ [8, 0],
149
+ [9, 0],
150
+ [10, 1],
151
+ [11, 0],
152
+ [14, 0],
153
+ [15, 1],
154
+ ];
155
+ /**
156
+ * A matrix in two dimensions as the move, turn, skew and scale it is, in that order, as CSS
157
+ * decomposes one: numbers native's transform node takes. Null for one in three dimensions, or
158
+ * one that flattens the view to a line, which are painted whole.
159
+ *
160
+ * A matrix painted whole is a list in a list, and Android's `overrideBySynchronousMountProps`,
161
+ * which puts what native last set back over each commit carrying a transform, copies back only
162
+ * the numbers and strings of each entry: the matrix comes back as nothing, and the view is drawn
163
+ * where it was never moved.
164
+ */
165
+ function flatMatrix(raw) {
166
+ if (!Array.isArray(raw) || raw.length !== 16 || !raw.every(Number.isFinite))
167
+ return null;
168
+ const m = raw;
169
+ if (FLAT_REST.some(([at, value]) => m[at] !== value))
170
+ return null;
171
+ const [a, b, c, d] = [m[FLAT.a], m[FLAT.b], m[FLAT.c], m[FLAT.d]];
172
+ const scaleX = Math.hypot(a, b);
173
+ if (scaleX === 0)
174
+ return null;
175
+ const [cos, sin] = [a / scaleX, b / scaleX];
176
+ const shear = cos * c + sin * d;
177
+ const scaleY = cos * (d - sin * shear) - sin * (c - cos * shear);
178
+ if (scaleY === 0)
179
+ return null;
180
+ return [
181
+ { property: 'translateX', value: m[FLAT.e] },
182
+ { property: 'translateY', value: m[FLAT.f] },
183
+ { property: 'rotate', value: Math.atan2(sin, cos) },
184
+ { property: 'skewX', value: Math.atan(shear / scaleY) },
185
+ { property: 'scaleX', value: scaleX },
186
+ { property: 'scaleY', value: scaleY },
187
+ ];
188
+ }
189
+ /** Whether a transform moves by a share of the box, which `paintedTransform` makes points of. */
190
+ function sharesBox(value) {
191
+ return (typeof value === 'string' ? value : JSON.stringify(value)).includes('%');
192
+ }
193
+ /** A transform as a commit carries it, a list, for native to be given whole. */
194
+ function literalTransform(value) {
195
+ if (typeof value === 'string')
196
+ return transformOf(value);
197
+ return Array.isArray(value) ? value : [];
198
+ }
199
+ /** Whether there is anything to paint in a set of values. */
200
+ function hasPaint(values) {
201
+ if (values.opacity !== undefined || values.transform !== undefined)
202
+ return true;
203
+ return Object.keys(values.colors ?? {}).length > 0;
204
+ }
205
+ /** A move by a share of the box, `translateX(-40%)`, as the points it is of the box. */
206
+ function shareInPoints(property, raw, boxOf) {
207
+ const side = SHARED_SIDE[property];
208
+ if (!side || typeof raw !== 'string' || !raw.endsWith('%'))
209
+ return null;
210
+ const box = boxOf();
211
+ return box ? (box[side] * Number.parseFloat(raw)) / 100 : null;
212
+ }
213
+ /** As Animated does: a view native paints is kept, or there is no view for it to paint. */
214
+ function keptForPaint(node, props) {
215
+ if (node.paint && props['collapsable'] === undefined)
216
+ props['collapsable'] = false;
217
+ }
90
218
  /**
91
219
  * The scroll events that mean a finger is dragging the list rather than tapping something in it.
92
220
  *
@@ -129,6 +257,8 @@ export const DIRECT_EVENTS = new Set([
129
257
  'topWillDisappear',
130
258
  'topDisappear',
131
259
  'topHeaderHeightChange',
260
+ // Sent on the screen whose arrow it is: one bubbling to the screen a stack sits in pops that too.
261
+ 'topHeaderBackButtonClicked',
132
262
  'topTransitionProgress',
133
263
  'topFinishTransitioning',
134
264
  // The tabs host reports its own selection, and only the host it happened in wants to know.
@@ -197,6 +327,7 @@ const VIEW_EVENT_OPT_IN_PROPS = {
197
327
  topContentSizeChange: 'onContentSizeChange',
198
328
  topScroll: 'onScroll',
199
329
  topKeyPress: 'onKeyPress',
330
+ topSelectionChange: 'onSelectionChange',
200
331
  },
201
332
  };
202
333
  /**
@@ -308,6 +439,17 @@ export function declareNativeProps(elementName, props) {
308
439
  export function claimHost(node) {
309
440
  node.claimed = true;
310
441
  }
442
+ /**
443
+ * Make a view the page of what is inside it: a `position: fixed` box there is placed against this
444
+ * view and not the screen, as it is against a screen of a stack, and follows it as it is given
445
+ * another size. For a layer that is the window to what it holds and need not be the size of the
446
+ * screen. Marked before anything fixed is put in it: it is for as long as the view is. Not for
447
+ * a view that scrolls its overflow: what is fixed in it is committed in what scrolls, and would
448
+ * scroll with it.
449
+ */
450
+ export function markPage(node) {
451
+ node.page = true;
452
+ }
311
453
  /**
312
454
  * Say that an element scrolls what overflows it, as a browser's does: with `overflow: auto` or
313
455
  * `scroll` it is committed as a scroll view, around one view that holds what it holds. For a
@@ -1442,11 +1584,13 @@ const GRID_KEYS = [
1442
1584
  'justifyItems',
1443
1585
  'justifySelf',
1444
1586
  ];
1587
+ /** Each inset a box can be given, the logical ones too. */
1588
+ const INSET_SIDES = ['top', 'right', 'bottom', 'left', 'start', 'end'];
1445
1589
  /** What places a box among its siblings, which the grid does for an item instead. */
1446
1590
  const PLACING_KEYS = [
1447
1591
  ...['margin', 'marginVertical', 'marginHorizontal'],
1448
1592
  ...['marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'marginStart', 'marginEnd'],
1449
- ...['top', 'right', 'bottom', 'left', 'start', 'end'],
1593
+ ...INSET_SIDES,
1450
1594
  ...['flex', 'flexGrow', 'flexShrink', 'flexBasis', 'alignSelf'],
1451
1595
  ];
1452
1596
  const SIZE_LIMITS = ['minWidth', 'maxWidth', 'minHeight', 'maxHeight'];
@@ -1481,6 +1625,15 @@ function gridLayout(node, props) {
1481
1625
  if (node.grid && props['position'] === 'static')
1482
1626
  props['position'] = 'relative';
1483
1627
  }
1628
+ // A transform, a filter and the rest make a box what its absolute boxes are placed against,
1629
+ // static or not, and Yoga knows only positions: see `placesItself`.
1630
+ if (props['position'] === 'static' && containsAbsolutes((key) => props[key])) {
1631
+ props['position'] = 'relative';
1632
+ // Still static to what places it: an inset moves no static box, in a grid or out of one.
1633
+ for (const key of INSET_SIDES)
1634
+ if (props[key] !== undefined)
1635
+ delete props[key];
1636
+ }
1484
1637
  const state = gridOver(node);
1485
1638
  if (state)
1486
1639
  gridItem(node, state, props);
@@ -1624,9 +1777,16 @@ function sharesRow(node) {
1624
1777
  * the flow is a share of a box it has no part in the width of, a focus ring over a chip.
1625
1778
  */
1626
1779
  function sharesWidth(node) {
1627
- if (!isShare(readStyle(node, 'width')) && !isShare(readStyle(node, 'minWidth')))
1628
- return false;
1629
- return !OUT_OF_FLOW.has(readStyle(node, 'position'));
1780
+ const share = isShare(readStyle(node, 'width')) || isShare(readStyle(node, 'minWidth')) || basisOfRow(node);
1781
+ return share && !OUT_OF_FLOW.has(readStyle(node, 'position'));
1782
+ }
1783
+ /**
1784
+ * Whether a box's basis is a share of the row it is an item of, which is a share of its width
1785
+ * as a percentage width is: a chip's cell, `flex-basis: 100%` in the chip.
1786
+ */
1787
+ function basisOfRow(node) {
1788
+ const row = node.parent;
1789
+ return row?.nodeKind === 'element' && isShare(readStyle(node, 'flexBasis')) && isRow(row);
1630
1790
  }
1631
1791
  /** A percentage: of the box a node is in, for a width or the least it may be. */
1632
1792
  const isShare = (value) => typeof value === 'string' && value.endsWith('%');
@@ -1693,13 +1853,23 @@ const engineMoves = (value) => Array.isArray(value) ? value.filter((entry) => EN
1693
1853
  const sameEntries = (a, b) => a === b || (Array.isArray(a) && a.length === b.length && a.every((entry, i) => entry === b[i]));
1694
1854
  /**
1695
1855
  * Take off what is read here and is no style of a native view: `order`, by the container in
1696
- * `inOrder`, and `justify-self`, by the block a box is in.
1856
+ * `inOrder`, `justify-self`, by the block a box is in, `table-layout`, by what lays a table out,
1857
+ * the band a library's sheet clips a box to, by what draws an image through it, and what makes a
1858
+ * box what its absolute boxes are placed against, by `containsAbsolutes`. The band is
1859
+ * `clipBand`: `clipPath` is the clip path a react-native-svg shape names, which reaches native.
1697
1860
  */
1698
1861
  function notNative(props) {
1699
1862
  if (props['order'] !== undefined)
1700
1863
  delete props['order'];
1701
1864
  if (props['justifySelf'] !== undefined)
1702
1865
  delete props['justifySelf'];
1866
+ if (props['tableLayout'] !== undefined)
1867
+ delete props['tableLayout'];
1868
+ if (props['clipBand'] !== undefined)
1869
+ delete props['clipBand'];
1870
+ for (const key of CONTAINING_ONLY)
1871
+ if (props[key] !== undefined)
1872
+ delete props[key];
1703
1873
  }
1704
1874
  /**
1705
1875
  * What the engine lays out in Yoga's place, written on a node's props last of all: a box sized
@@ -1983,11 +2153,56 @@ function mixedValues(parts, [width, height], own, inColumn = false) {
1983
2153
  const SIZE_KEYS = new Set([
1984
2154
  ...['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight', 'flexBasis'],
1985
2155
  ]);
2156
+ /** What makes a box a containing block that no native view has a style for: `containsAbsolutes`. */
2157
+ const CONTAINING_ONLY = ['willChange', 'contain', 'perspective', 'backdropFilter'];
1986
2158
  /**
1987
2159
  * What only the engine reads, whatever it is set to: no native view lays out by any of them, or
1988
- * draws by `visibility` or a clip to nothing (`seenByCss`).
2160
+ * draws by `visibility`, a clip to nothing (`seenByCss`) or a clip to a band (`notNative`).
2161
+ */
2162
+ const LAID_OUT_HERE = new Set([
2163
+ ...GRID_KEYS,
2164
+ 'order',
2165
+ 'visibility',
2166
+ 'clipped',
2167
+ 'tableLayout',
2168
+ 'clipBand',
2169
+ ...CONTAINING_ONLY,
2170
+ ]);
2171
+ /**
2172
+ * Whether a box is what an absolute box in it is placed against: one that is not static, or one
2173
+ * CSS makes a containing block however it is positioned (see `containsAbsolutes`).
2174
+ */
2175
+ function placesItself(node) {
2176
+ return (readStyle(node, 'position') !== 'static' || containsAbsolutes((key) => readStyle(node, key)));
2177
+ }
2178
+ /**
2179
+ * Whether a style makes its box the containing block of its absolute boxes, positioned or not: a
2180
+ * transform, a filter, a backdrop filter or a perspective other than `none`, a containment of
2181
+ * layout or paint, or a `will-change` naming one of those or `position`.
1989
2182
  */
1990
- const LAID_OUT_HERE = new Set([...GRID_KEYS, 'order', 'visibility', 'clipped']);
2183
+ function containsAbsolutes(style) {
2184
+ if (CONTAINING_KEYS.some((key) => given(style(key))))
2185
+ return true;
2186
+ const contain = style('contain');
2187
+ if (typeof contain === 'string' && contain.split(/\s+/).some((word) => CONTAINS.has(word)))
2188
+ return true;
2189
+ const willChange = style('willChange');
2190
+ return (typeof willChange === 'string' &&
2191
+ willChange.split(',').some((name) => WILL_CONTAIN.has(name.trim())));
2192
+ }
2193
+ /** What makes a box a containing block where it is set to anything but `none`. */
2194
+ const CONTAINING_KEYS = ['transform', 'filter', 'backdropFilter', 'perspective'];
2195
+ /** The containments that make one: of layout or paint, alone or as `strict` or `content`. */
2196
+ const CONTAINS = new Set(['layout', 'paint', 'strict', 'content']);
2197
+ /** The properties a `will-change` makes one by naming, as Chrome has it. */
2198
+ const WILL_CONTAIN = new Set([
2199
+ ...['transform', 'translate', 'rotate', 'scale', 'perspective'],
2200
+ ...['filter', 'backdrop-filter', 'contain', 'position'],
2201
+ ]);
2202
+ /** Whether a value is one: a list or text of one, not `none`, or a number. */
2203
+ const given = (value) => Array.isArray(value)
2204
+ ? value.length > 0
2205
+ : typeof value === 'number' || (typeof value === 'string' && value !== '' && value !== 'none');
1991
2206
  const POSITIONS_HERE = new Set(['static', 'fixed', 'sticky']);
1992
2207
  /** What a declaration can ask of the engine that an app of plain flex does not: see `asks`. */
1993
2208
  const CSS_LAYOUT = 1;
@@ -2047,9 +2262,10 @@ const SHARES_OF = {
2047
2262
  minWidth: SHARED_WIDTHS,
2048
2263
  height: SHARED_HEIGHTS,
2049
2264
  minHeight: SHARED_HEIGHTS,
2050
- flexBasis: SHARED_HEIGHTS,
2265
+ // Of a column's height, or of a row's width: which, the box it is in says.
2266
+ flexBasis: SHARED_HEIGHTS | SHARED_WIDTHS,
2051
2267
  };
2052
- const heightShare = (key) => SHARES_OF[key] === SHARED_HEIGHTS ? SHARED_HEIGHTS : 0;
2268
+ const heightShare = (key) => (SHARES_OF[key] ?? 0) & SHARED_HEIGHTS;
2053
2269
  /** What a size asks for: a share of its box, an automatic minimum, or a keyword of CSS's. */
2054
2270
  function asksBySize(key, value) {
2055
2271
  const least = key === 'minWidth' || key === 'minHeight';
@@ -2488,7 +2704,7 @@ function defined(css) {
2488
2704
  * margins (see `hang`), on the bottom of the row the line is.
2489
2705
  */
2490
2706
  function lineStrut(inherited, container, first, fontScale) {
2491
- if (risenLine(container, first))
2707
+ if (risenLine(container, first) || emptyLine(container, first))
2492
2708
  return RISEN;
2493
2709
  const under = underLine(container, first);
2494
2710
  const above = strutOf(inherited).above * textScale(inherited, fontScale);
@@ -2535,6 +2751,56 @@ function knownHeight(style, whole) {
2535
2751
  const risenLine = (container, first) => container.flow?.plan.lines.get(first)?.boxes.some((box) => container.flow?.rises?.has(box)) ===
2536
2752
  true;
2537
2753
  const RISEN = { minHeight: 0, alignContent: 'flex-start' };
2754
+ /** A block whose lines have nothing on them: no room is kept for any. */
2755
+ function nothingOnLines(flow) {
2756
+ flow.lifts = new Map();
2757
+ flow.unders = new Map();
2758
+ flow.rises = new Map();
2759
+ flow.risen = true;
2760
+ if (flow.plan.kind === 'lines')
2761
+ flow.under = 0;
2762
+ }
2763
+ /** A line among blocks with nothing on it: see `addsNothing`. */
2764
+ const emptyLine = (container, first) => container.flow?.plan.lines.get(first)?.boxes.every(addsNothing) === true;
2765
+ /**
2766
+ * Whether a box puts nothing on a line: an element a browser lays out inline that holds
2767
+ * nothing, or only more such elements, with no size, padding, border or margin of its own. A
2768
+ * line of nothing else has no height (CSS 2.2, 9.4.2): a component's empty host after a field,
2769
+ * a datepicker's, which draws in an overlay, adds no strut under the field.
2770
+ */
2771
+ function addsNothing(node) {
2772
+ if (!node.inlineLevel)
2773
+ return false;
2774
+ // A display of its own, an inline-block written in a sheet, is a box on the line however
2775
+ // empty: only the one the engine gives an inline element is not.
2776
+ if (ownStyle(node, 'display') != null)
2777
+ return false;
2778
+ // Nothing in it but more of the same, and text of no length.
2779
+ for (const child of node.childList) {
2780
+ if (child.nodeKind === 'text' ? child.nodeText !== '' : !emptyOrNotABox(child))
2781
+ return false;
2782
+ }
2783
+ return BOX_KEYS.every((key) => {
2784
+ const value = readStyle(node, key);
2785
+ return value == null || value === 0 || value === 'auto';
2786
+ });
2787
+ }
2788
+ /** Whether a node in an inline element adds nothing to its line: see `addsNothing`. */
2789
+ const emptyOrNotABox = (node) => node.nodeKind !== 'element' || addsNothing(node);
2790
+ /** What gives a box a size on a line, whatever it holds. */
2791
+ const BOX_KEYS = [
2792
+ ...['width', 'height', 'minWidth', 'minHeight', 'flexBasis', 'aspectRatio'],
2793
+ ...['padding', 'paddingHorizontal', 'paddingVertical', 'paddingTop', 'paddingBottom'],
2794
+ ...['paddingLeft', 'paddingRight', 'paddingStart', 'paddingEnd'],
2795
+ ...['margin', 'marginHorizontal', 'marginVertical', 'marginTop', 'marginBottom'],
2796
+ ...['marginLeft', 'marginRight', 'marginStart', 'marginEnd'],
2797
+ ...['borderWidth', 'borderTopWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRightWidth'],
2798
+ ...['borderStartWidth', 'borderEndWidth'],
2799
+ ...['paddingBlock', 'paddingBlockStart', 'paddingBlockEnd'],
2800
+ ...['paddingInline', 'paddingInlineStart', 'paddingInlineEnd'],
2801
+ ...['marginBlock', 'marginBlockStart', 'marginBlockEnd'],
2802
+ ...['marginInline', 'marginInlineStart', 'marginInlineEnd'],
2803
+ ];
2538
2804
  /** The room under the baseline of a line among blocks, by its first box. */
2539
2805
  const underLine = (container, first) => container.flow?.unders?.get(first) ?? 0;
2540
2806
  const sameStrut = (a, b) => a?.minHeight === b.minHeight && a.alignContent === b.alignContent;
@@ -2869,7 +3135,12 @@ function strutted(node, props) {
2869
3135
  (isSet(props['aspectRatio']) && props['aspectRatio'] !== 'auto');
2870
3136
  // Between two insets an out-of-flow box is as tall as they leave it.
2871
3137
  const held = OUT_OF_FLOW.has(props['position']) && isSet(props['top']) && isSet(props['bottom']);
2872
- return !tall && !held && !holdsText(node);
3138
+ if (tall || held || holdsText(node))
3139
+ return false;
3140
+ // No line at all, in a block that holds nothing, or lines with nothing on them: see
3141
+ // `addsNothing`.
3142
+ const boxes = node.childList.filter((child) => child.nodeKind === 'element');
3143
+ return !boxes.every(addsNothing);
2873
3144
  }
2874
3145
  /**
2875
3146
  * Whether a generated box has text for its content, a space included: nothing is seen of a
@@ -2921,6 +3192,10 @@ function styleKey(value, key) {
2921
3192
  }
2922
3193
  /** What a node's own style says for `key`, as `ownLayout` answers it, without an allocation. */
2923
3194
  function readStyle(node, key) {
3195
+ return ownStyle(node, key) ?? node.defaultStyle?.[key];
3196
+ }
3197
+ /** The same, without the default an element is given for its name or by the engine. */
3198
+ function ownStyle(node, key) {
2924
3199
  const props = node.props;
2925
3200
  // Asked of most nodes more than once a commit, and most have neither of the first two.
2926
3201
  const over = props[STYLE_OVERRIDE];
@@ -2934,8 +3209,7 @@ function readStyle(node, key) {
2934
3209
  ? inline[key]
2935
3210
  : styleKey(inline, key)) ??
2936
3211
  props[key] ??
2937
- node.styleCache?.style[key] ??
2938
- node.defaultStyle?.[key]);
3212
+ node.styleCache?.style[key]);
2939
3213
  }
2940
3214
  /** Whether a plan lays `child` out the same way in both: in the flow, with the same margins. */
2941
3215
  function sameFlow(child, before, after) {
@@ -3713,6 +3987,10 @@ const UNCAPPED = 1000;
3713
3987
  const FONT_REFRESH_STEP = 0.006;
3714
3988
  /** The views that take a cursor, on either platform. */
3715
3989
  const TEXT_INPUTS = new Set(['TextInput', 'AndroidTextInput']);
3990
+ /** Whether a view name is a text input's, for a committed view, which has a name and no node. */
3991
+ export const isTextInputView = (viewName) => TEXT_INPUTS.has(viewName);
3992
+ /** Whether a node commits as a text input, the view that takes a cursor, on either platform. */
3993
+ export const isTextInput = (node) => isTextInputView(viewNameOf(node));
3716
3994
  /**
3717
3995
  * Android's horizontal scroll view and the view that holds its content. RN's `ScrollView.js`
3718
3996
  * commits them in place of `ScrollView` and its plain content view when `horizontal` is set, and
@@ -3923,6 +4201,16 @@ function stacksRuns(node) {
3923
4201
  }
3924
4202
  /** One run of text to place: text itself, or a text element around some, a label in a span. */
3925
4203
  const oneRun = (child) => child.nodeKind === 'text' || (child.nodeKind === 'element' && TEXT_ELEMENTS.has(child.tag));
4204
+ /**
4205
+ * The node whose view native has a node's view in, through a line box it is committed in, or
4206
+ * the node it is in where it is not committed.
4207
+ */
4208
+ function drawnIn(node) {
4209
+ const under = node.committedUnder;
4210
+ if (!under)
4211
+ return node.parent;
4212
+ return 'nodeKind' in under ? under : under.in;
4213
+ }
3926
4214
  /**
3927
4215
  * The view a node is committed as: `viewNameOf`, but for a text element that aligns its text, and
3928
4216
  * for the content view of a horizontal scroll view on Android, which RN commits as a view of its
@@ -4106,6 +4394,8 @@ function laidByNative(node, key) {
4106
4394
  return true;
4107
4395
  return node.playing?.laid === true && node.playing.tracks.has(key);
4108
4396
  }
4397
+ /** Whether an animation stays at its last frame once it has ended: by its fill, or held there. */
4398
+ const fillsForwards = (running) => running.held === true || running.spec.fill === 'forwards' || running.spec.fill === 'both';
4109
4399
  /** Whether an animation plays once through, forwards, with no wait before it. */
4110
4400
  const playedOnce = (spec) => spec.delay === 0 && spec.duration > 0 && spec.iterations === 1 && !spec.direction;
4111
4401
  /** Whether a track is two frames on one curve, the first of them where the view is drawn. */
@@ -4143,6 +4433,26 @@ const EASED_COLOURS = new Set([
4143
4433
  'borderLeftColor',
4144
4434
  ]);
4145
4435
  const NATIVELY_EASED = new Set(['opacity', 'transform', ...EASED_COLOURS]);
4436
+ const SIDE_COLOURS = ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'];
4437
+ /**
4438
+ * Ease a view's unsided border colour with its four sides where they ease alike: Android dashes
4439
+ * a rounded border only from `borderColor`, and only while every side equals it. Asked of what
4440
+ * the view is committed with, which the element's own sides can make uniform too.
4441
+ */
4442
+ function unsidedBorderTrack(tracks, props) {
4443
+ if (tracks.has('borderColor'))
4444
+ return;
4445
+ const [top, ...rest] = SIDE_COLOURS.map((key) => tracks.get(key));
4446
+ if (!top || rest.some((track) => !sameTrack(track, top)))
4447
+ return;
4448
+ const settled = { ...props };
4449
+ settleUnsidedBorder(settled);
4450
+ if (settled['borderColor'] !== undefined)
4451
+ tracks.set('borderColor', top);
4452
+ }
4453
+ const sameTrack = (one, other) => one !== undefined &&
4454
+ one.length === other.length &&
4455
+ one.every((point, at) => point.value === other[at].value);
4146
4456
  /** What a node that hands its background colour over has handed so far: nothing. */
4147
4457
  const UNREPORTED = Symbol('unreported');
4148
4458
  /** The style keys that are a view's background. */
@@ -4584,6 +4894,7 @@ const ENGINE_NODE_FIELDS = {
4584
4894
  resolved: true,
4585
4895
  sticky: true,
4586
4896
  fixedTo: true,
4897
+ page: true,
4587
4898
  ordered: true,
4588
4899
  fit: true,
4589
4900
  fitMin: true,
@@ -4604,6 +4915,8 @@ const ENGINE_NODE_FIELDS = {
4604
4915
  playing: true,
4605
4916
  scrolled: true,
4606
4917
  owned: true,
4918
+ paint: true,
4919
+ unsynced: true,
4607
4920
  composedTransform: true,
4608
4921
  bornIn: true,
4609
4922
  boundTransform: true,
@@ -4627,10 +4940,12 @@ const ENGINE_NODE_FIELDS = {
4627
4940
  oneWord: true,
4628
4941
  raised: true,
4629
4942
  presented: true,
4943
+ held: true,
4630
4944
  propsDirty: true,
4631
4945
  structureDirty: true,
4632
4946
  subtreeDirty: true,
4633
4947
  inside: true,
4948
+ contentView: true,
4634
4949
  scrollsOverflow: true,
4635
4950
  engine: true,
4636
4951
  };
@@ -4738,7 +5053,11 @@ const colorProps = new Map();
4738
5053
  function isColorProp(key) {
4739
5054
  let answer = colorProps.get(key);
4740
5055
  if (answer === undefined) {
4741
- colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || STATE_COLOR_PROPS.has(key) || BRUSH_PROPS.has(key)));
5056
+ colorProps.set(key, (answer =
5057
+ IS_COLOR_PROP.test(key) ||
5058
+ STATE_COLOR_PROPS.has(key) ||
5059
+ BRUSH_PROPS.has(key) ||
5060
+ COLOR_LIST_PROPS.has(key)));
4742
5061
  }
4743
5062
  return answer;
4744
5063
  }
@@ -4752,6 +5071,11 @@ const ASSET_PROPS = new Set(['source', 'defaultSource', 'loadingIndicatorSource'
4752
5071
  * approximated, it is dropped.
4753
5072
  */
4754
5073
  const NESTED_COLOR_LIST_PROPS = new Set(['boxShadow']);
5074
+ /**
5075
+ * Props whose value is a list of colours: the spinner colours of Android's swipe layout and an SF
5076
+ * Symbol's palette, each of which React Native or Expo runs through `processColorArray`.
5077
+ */
5078
+ const COLOR_LIST_PROPS = new Set(['colors']);
4755
5079
  /** The props of an SVG shape that take a brush, not a colour. */
4756
5080
  const BRUSH_PROPS = new Set(['fill', 'stroke']);
4757
5081
  /** The brush that paints a shape in the colour of the view it is in: `currentColor`. */
@@ -5257,6 +5581,7 @@ export class Engine {
5257
5581
  worstRenderMs: 0,
5258
5582
  createdNodes: 0,
5259
5583
  clonedNodes: 0,
5584
+ paintedNatively: 0,
5260
5585
  };
5261
5586
  eventsRegistered = false;
5262
5587
  root = new RetainedNode('element', 'root', this);
@@ -5267,6 +5592,7 @@ export class Engine {
5267
5592
  styles;
5268
5593
  /** Whether to run the development checks, which a release build skips. */
5269
5594
  dev;
5595
+ /** The clock transitions and animations run on, which a script reads where one has got to. */
5270
5596
  now;
5271
5597
  onDirty;
5272
5598
  onError;
@@ -5344,6 +5670,7 @@ export class Engine {
5344
5670
  if (this.dev) {
5345
5671
  this.styles.onUndefinedToken = (name, props, on) => this.reportUndefinedToken(name, props, on);
5346
5672
  this.styles.onUnreadDisplay = (name, value) => this.reportUnreadDisplay(name, value);
5673
+ this.styles.onPlatformColour = (name, props, on) => this.reportPlatformColour(name, props, on);
5347
5674
  }
5348
5675
  this.now = options.now ?? (() => globalThis.performance?.now?.() ?? Date.now());
5349
5676
  this.onDirty = options.onDirty;
@@ -5354,6 +5681,10 @@ export class Engine {
5354
5681
  this.scrollDriver = options.nativeAnimated
5355
5682
  ? new NativeScrollDriver(options.nativeAnimated, (view, tag, keys) => this.touchedNatively(view, tag, keys))
5356
5683
  : null;
5684
+ // The painter's graph takes the props it paints from commits as any graph does: see `Touched`.
5685
+ this.painter = options.nativeAnimated
5686
+ ? new NativePainter(options.nativeAnimated, (view, tag, keys) => this.touchedNatively(view, tag, keys))
5687
+ : null;
5357
5688
  // Eagerly, not on the first listener. Fabric holds exactly one event handler and
5358
5689
  // `ReactFabric` installs React's at module scope, so until we claim it every native event
5359
5690
  // lands in React's event plugins. Those look the name up in a view config registry we never
@@ -5821,6 +6152,73 @@ export class Engine {
5821
6152
  this.generate(node);
5822
6153
  return node;
5823
6154
  }
6155
+ /**
6156
+ * Commit a node without it being seen or touched, or let it be again. A browser paints nothing
6157
+ * until a task and its animation frame are over, and native draws each commit as it comes: what
6158
+ * a library inserts, measures and only then styles is held for the frame between. Over every
6159
+ * opacity and `pointerEvents` it has, an eased one included, which it is committed with again
6160
+ * once let go. Nothing it matches changes, so nothing is styled again.
6161
+ */
6162
+ hold(node, held) {
6163
+ if ((node.held === true) === held)
6164
+ return;
6165
+ if (held)
6166
+ node.held = true;
6167
+ else
6168
+ delete node.held;
6169
+ this.markProps(node, false);
6170
+ }
6171
+ /**
6172
+ * Stop showing the last frame of a node's finished `@keyframes` animation, where nothing holds
6173
+ * it there any more: script took its hold away after it ended. The node is what the cascade
6174
+ * gives it again with the next commit.
6175
+ */
6176
+ releaseFinished(node) {
6177
+ const running = node.playing;
6178
+ if (!running?.done || fillsForwards(running))
6179
+ return;
6180
+ running.values = {};
6181
+ this.markProps(node, false);
6182
+ }
6183
+ /**
6184
+ * End a node's `@keyframes` animation now, as a script's `finish()` or `cancel()` does: at its
6185
+ * end, which is its last frame where it fills forwards and its resting style where it does not,
6186
+ * or with no effect of it at all. Native is stopped where it plays it, and what the view is drawn
6187
+ * at set there too (see `endedNatively`). It stays ended while the node asks for the same one.
6188
+ */
6189
+ endPlaying(node, how) {
6190
+ const running = node.playing;
6191
+ // One that never ends has no end to go to: a script is refused it first.
6192
+ if (!running || (how === 'finish' && running.spec.iterations === null))
6193
+ return;
6194
+ if (running.done) {
6195
+ // Ended already, and held at its last frame by its fill: a cancel lets that go.
6196
+ if (how === 'cancel' && Object.keys(running.values).length) {
6197
+ running.values = {};
6198
+ this.markProps(node, false);
6199
+ }
6200
+ return;
6201
+ }
6202
+ const native = running.native !== undefined;
6203
+ this.stopNative(node, running);
6204
+ running.pausedAt = undefined;
6205
+ if (how === 'finish') {
6206
+ const { spec } = running;
6207
+ const end = running.start + spec.delay + spec.duration * (spec.iterations ?? 1);
6208
+ this.advancePlayer(node, Math.max(this.now(), end));
6209
+ }
6210
+ else {
6211
+ running.values = {};
6212
+ running.done = true;
6213
+ this.playing.delete(node);
6214
+ this.markProps(node, false);
6215
+ this.emitTransition(node, 'topAnimationcancel', running.spec.name);
6216
+ }
6217
+ this.commit();
6218
+ const committed = node.committed;
6219
+ if (native && node.owned && committed)
6220
+ this.setOwned(node, RESET_OWNED, committed.props);
6221
+ }
5824
6222
  addClass(node, name) {
5825
6223
  const was = this.classAttribute(node);
5826
6224
  (node.classes ??= new Set()).add(name);
@@ -6098,7 +6496,8 @@ export class Engine {
6098
6496
  node.nodeText = value;
6099
6497
  return node;
6100
6498
  }
6101
- appendChild(parent, child) {
6499
+ appendChild(into, child) {
6500
+ const parent = into.contentView && into.contentView !== child ? into.contentView : into;
6102
6501
  this.markAdopted(child, parent);
6103
6502
  this.detach(child);
6104
6503
  this.markReturned(child);
@@ -6113,7 +6512,8 @@ export class Engine {
6113
6512
  this.markStructure(parent, child, parent.childList.length - 1);
6114
6513
  this.markTextContent(parent, child);
6115
6514
  }
6116
- insertBefore(parent, child, ref) {
6515
+ insertBefore(into, child, ref) {
6516
+ const parent = into.contentView && into.contentView !== child ? into.contentView : into;
6117
6517
  this.markAdopted(child, parent);
6118
6518
  this.detach(child);
6119
6519
  this.markReturned(child);
@@ -6146,6 +6546,10 @@ export class Engine {
6146
6546
  if (at < 0)
6147
6547
  return;
6148
6548
  this.carryHoists(child);
6549
+ if (this.holdsMoves)
6550
+ this.holdMove(target, child, at);
6551
+ if (this.heldMoves.size)
6552
+ this.releaseHeldUnder(child);
6149
6553
  target.childList.splice(at, 1);
6150
6554
  child.parent = null;
6151
6555
  this.outSince.set(child, this.classesAbove);
@@ -6309,6 +6713,9 @@ export class Engine {
6309
6713
  /** Forget the committed views of each node removed since the last commit that is still out. */
6310
6714
  forgetDetached() {
6311
6715
  for (const node of this.removedSinceCommit) {
6716
+ // One a finger is on goes once the finger lifts: see `holdMove`.
6717
+ if (this.heldMoves.has(node))
6718
+ continue;
6312
6719
  if (node.committed && this.topOf(node) !== this.root)
6313
6720
  this.forgetCommitted(node);
6314
6721
  }
@@ -6335,6 +6742,8 @@ export class Engine {
6335
6742
  // as. Native gives a view one parent for its life, and aborts on a second.
6336
6743
  if (node.committed)
6337
6744
  this.forgetCommitted(node);
6745
+ // Destroyed, it is held no longer: it is gone from native with its parent's next commit.
6746
+ this.heldMoves.delete(node);
6338
6747
  this.stopEased(node);
6339
6748
  node.transitions = undefined;
6340
6749
  if (node.playing)
@@ -7172,6 +7581,8 @@ export class Engine {
7172
7581
  this.placesMarkedAfter.clear();
7173
7582
  if (!this.root.structureDirty && !this.root.subtreeDirty)
7174
7583
  return false;
7584
+ if (this.heldMoves.size)
7585
+ this.markHolders();
7175
7586
  const started = now();
7176
7587
  // Faces registered before the walk, the global sheet's among them, reach every node in it.
7177
7588
  this.facesAdded = false;
@@ -7183,13 +7594,19 @@ export class Engine {
7183
7594
  this.quietly.clear();
7184
7595
  this.loudly.clear();
7185
7596
  this.holders = undefined;
7597
+ const sent = this.rootsSent;
7598
+ this.paintedInstead = false;
7186
7599
  this.commitAsAsked();
7600
+ if (this.paintedOnly(sent))
7601
+ return false;
7187
7602
  // Each a layout of the engine's, which an app of plain flex has asked for none of.
7188
7603
  if (!this.paintOnly && this.asked & LAYOUTS)
7189
7604
  this.settleLayout();
7190
7605
  this.paintOnly = false;
7191
7606
  this.changesHeld = true;
7192
7607
  this.countCommit(now() - started);
7608
+ if (this.paintedNodes.size)
7609
+ this.repaintShares();
7193
7610
  this.flushTransitionEvents();
7194
7611
  this.reportPaint();
7195
7612
  this.playSized();
@@ -7203,6 +7620,17 @@ export class Engine {
7203
7620
  this.blurHidden();
7204
7621
  return true;
7205
7622
  }
7623
+ /**
7624
+ * Whether a commit sent native nothing, every change in it being paint native was given
7625
+ * directly: see `paintNatively`. Nothing was laid out, so nothing is read back.
7626
+ */
7627
+ paintedOnly(sentBefore) {
7628
+ if (this.rootsSent !== sentBefore || !this.paintedInstead)
7629
+ return false;
7630
+ this.paintOnly = false;
7631
+ this.flushTransitionEvents();
7632
+ return true;
7633
+ }
7206
7634
  /** Count a commit that reached native, and how long it took, in `stats`. */
7207
7635
  countCommit(elapsed) {
7208
7636
  this.stats.commits++;
@@ -7605,12 +8033,23 @@ export class Engine {
7605
8033
  // What that found resized is the transitions themselves, at the frame they are eased to.
7606
8034
  this.movesOthers = false;
7607
8035
  }
8036
+ this.clearFlags(this.root);
8037
+ // The same views as the last commit, where what changed was painted by native instead: as
8038
+ // before otherwise, since a commit of nothing new is what has a box read back again.
8039
+ if (this.paintedInstead && this.sentHandles && sameHandles(handles, this.sentHandles))
8040
+ return;
8041
+ this.sentHandles = handles;
8042
+ this.rootsSent++;
7608
8043
  for (const handle of handles)
7609
8044
  this.fabric.appendChildToSet(set, handle);
7610
- this.clearFlags(this.root);
7611
8045
  this.announceCommit();
7612
8046
  this.fabric.completeRoot(this.rootTag, set);
7613
8047
  }
8048
+ /** The views the last `completeRoot` was given, and how many there have been. */
8049
+ sentHandles = null;
8050
+ /** Set in a commit where a change was given to native to paint in place of being committed. */
8051
+ paintedInstead = false;
8052
+ rootsSent = 0;
7614
8053
  /** The fixed boxes each page, or the root, is committed with. */
7615
8054
  fixedIn = new Map();
7616
8055
  /** Boxes found to be fixed while they were being committed under their parent. */
@@ -7619,6 +8058,109 @@ export class Engine {
7619
8058
  fingerOn = null;
7620
8059
  /** Boxes that became fixed or absolute with the finger on them, left under their parents. */
7621
8060
  fixedUnderFinger = new Set();
8061
+ /** Whether a node moved with a finger on it waits for the finger to lift: `holdMovesUnderFinger`. */
8062
+ holdsMoves = false;
8063
+ /**
8064
+ * The nodes taken out of the parent they are committed under with a finger on them, each with
8065
+ * that parent and the element that came after it there, which it stays before.
8066
+ */
8067
+ heldMoves = new Map();
8068
+ /**
8069
+ * Have a node that is moved to another parent, or taken out, while the first finger down is on
8070
+ * it or on something in it stay committed where it was until the last finger lifts, and then
8071
+ * go where it is. A native view moved to another parent is a view made again, which loses the
8072
+ * touch on it: a web library that drags an element moves it as the drag begins, out of its list
8073
+ * and on to the body, and its drag stopped there. The tree is moved at once, so what reads it
8074
+ * finds the node where it was put, and only what native is told waits. Off, what is held goes
8075
+ * where it is with the next commit.
8076
+ */
8077
+ holdMovesUnderFinger(on) {
8078
+ this.holdsMoves = on;
8079
+ if (!on && this.heldMoves.size)
8080
+ this.releaseMoves();
8081
+ }
8082
+ /** A node is taken out of `parent` from `at`: held there, where a finger is on it. */
8083
+ holdMove(parent, child, at) {
8084
+ if (this.heldMoves.has(child) || child.committed === null)
8085
+ return;
8086
+ // Under the root it is one of the root's handles, which hold no node from elsewhere.
8087
+ if (child.committedUnder !== parent || parent === this.root)
8088
+ return;
8089
+ if (!isWithin(this.fingerOn, child))
8090
+ return;
8091
+ const before = parent.childList.slice(at + 1).find((next) => next.nodeKind === 'element');
8092
+ this.heldMoves.set(child, { under: parent, before: before ?? null });
8093
+ }
8094
+ /** Whether a node is held under a parent other than the one it is in now. */
8095
+ heldElsewhere(child) {
8096
+ const held = this.heldMoves.get(child);
8097
+ return held !== undefined && held.under !== child.parent;
8098
+ }
8099
+ /**
8100
+ * Add to a node's child handles each node held under it that has gone from it, before the
8101
+ * element it came before where that is still there: reconciled where it is in the tree, so it
8102
+ * is drawn as it is styled, and as it was last committed once it is out of the tree.
8103
+ */
8104
+ heldHandles(node, handles) {
8105
+ for (const [child, held] of this.heldMoves) {
8106
+ if (held.under !== node || child.parent === node)
8107
+ continue;
8108
+ const handle = child.parent ? this.reconcileUnder(node, child) : child.committed?.handle;
8109
+ if (!handle)
8110
+ continue;
8111
+ const next = held.before?.committedUnder === node ? held.before.committed : null;
8112
+ const at = next ? handles.indexOf(next.handle) : -1;
8113
+ if (at < 0)
8114
+ handles.push(handle);
8115
+ else
8116
+ handles.splice(at, 0, handle);
8117
+ }
8118
+ }
8119
+ /**
8120
+ * Have each parent a node is held under committed with its children again: a change to the node
8121
+ * marks the way up from where it is in the tree, and not from where it is committed.
8122
+ */
8123
+ markHolders() {
8124
+ for (const { under } of this.heldMoves.values()) {
8125
+ for (let at = under; at && !at.subtreeDirty; at = at.parent) {
8126
+ at.subtreeDirty = true;
8127
+ }
8128
+ }
8129
+ }
8130
+ /**
8131
+ * A node is taken out of the page: each node held under it, or under what it holds, is let go,
8132
+ * as there is nowhere left to draw it where it was. One moved elsewhere is drawn there.
8133
+ */
8134
+ releaseHeldUnder(removed) {
8135
+ for (const [node, { under }] of [...this.heldMoves]) {
8136
+ if (node === removed || !isWithin(under, removed))
8137
+ continue;
8138
+ this.heldMoves.delete(node);
8139
+ if (node.parent) {
8140
+ node.parent.structureDirty = true;
8141
+ this.markPath(node.parent);
8142
+ }
8143
+ else
8144
+ this.removedSinceCommit.add(node);
8145
+ }
8146
+ }
8147
+ /** The last finger is up: each node held goes where it is with the next commit. */
8148
+ releaseMoves() {
8149
+ const held = [...this.heldMoves];
8150
+ this.heldMoves.clear();
8151
+ for (const [node, { under }] of held) {
8152
+ under.structureDirty = true;
8153
+ this.markPath(under);
8154
+ if (node.parent) {
8155
+ node.parent.structureDirty = true;
8156
+ this.markPath(node.parent);
8157
+ }
8158
+ else {
8159
+ // Out of the tree, it is let go of as any node taken out is: see `forgetDetached`.
8160
+ this.removedSinceCommit.add(node);
8161
+ }
8162
+ }
8163
+ }
7622
8164
  /** Note what a finger comes down on, and once the last has lifted move what waited for it. */
7623
8165
  trackFinger(target, type, nativeEvent) {
7624
8166
  const fingers = nativeEvent?.touches?.length ?? 0;
@@ -7628,6 +8170,8 @@ export class Engine {
7628
8170
  if ((type !== 'topTouchEnd' && type !== 'topTouchCancel') || fingers > 0)
7629
8171
  return;
7630
8172
  this.fingerOn = null;
8173
+ if (this.heldMoves.size)
8174
+ this.releaseMoves();
7631
8175
  for (const node of this.fixedUnderFinger) {
7632
8176
  if (!isWithin(node, this.root))
7633
8177
  continue;
@@ -7639,7 +8183,7 @@ export class Engine {
7639
8183
  /** The page a box is on: the nearest view that holds one, or the root. */
7640
8184
  pageOf(node) {
7641
8185
  let page = node.parent;
7642
- while (page?.parent && !FIXED_BOUNDARIES.has(viewNameOf(page))) {
8186
+ while (page?.parent && !page.page && !FIXED_BOUNDARIES.has(viewNameOf(page))) {
7643
8187
  page = page.parent;
7644
8188
  }
7645
8189
  return page;
@@ -8444,7 +8988,8 @@ export class Engine {
8444
8988
  }
8445
8989
  /**
8446
8990
  * The boxes inside `node` that Yoga sizes from the room on offer and not from their content:
8447
- * one with a percentage width, and one that takes a share of a row from a basis of nothing.
8991
+ * one with a percentage width or a percentage basis in a row, and one that takes a share of a
8992
+ * row from a basis of nothing.
8448
8993
  * Nothing under a box with a width of its own in points is looked at: that width is its
8449
8994
  * content's room whatever it holds.
8450
8995
  */
@@ -8454,7 +8999,10 @@ export class Engine {
8454
8999
  if (child.nodeKind !== 'element')
8455
9000
  continue;
8456
9001
  const width = readStyle(child, 'width');
8457
- const shares = typeof readStyle(child, 'flex') === 'number' || isNoBasis(readStyle(child, 'flexBasis'));
9002
+ const basis = readStyle(child, 'flexBasis');
9003
+ const shares = typeof readStyle(child, 'flex') === 'number' ||
9004
+ isNoBasis(basis) ||
9005
+ (isShare(basis) && isRow(node));
8458
9006
  if (typeof width === 'string' || shares || isShare(readStyle(child, 'minWidth'))) {
8459
9007
  found.push(child);
8460
9008
  }
@@ -8706,7 +9254,7 @@ export class Engine {
8706
9254
  const inGrid = () => node.parent?.grid !== undefined;
8707
9255
  // Under a box that is not positioned, it is placed against one further up, and a share of
8708
9256
  // a size is a share of that one's.
8709
- const through = () => node.parent != null && readStyle(node.parent, 'position') === 'static';
9257
+ const through = () => node.parent != null && !placesItself(node.parent);
8710
9258
  // In a block, or in a line of one: a container the engine lays out as a browser's flow.
8711
9259
  const block = node.parent?.flow !== undefined;
8712
9260
  if (position === 'fixed' || block || auto() || inGrid() || through())
@@ -8875,7 +9423,7 @@ export class Engine {
8875
9423
  /** The box Yoga positions an out-of-flow node by: the nearest ancestor that is not static. */
8876
9424
  containingBlock(node) {
8877
9425
  for (let up = node.parent; up; up = up.parent) {
8878
- if (up.nodeKind !== 'element' || readStyle(up, 'position') !== 'static')
9426
+ if (up.nodeKind !== 'element' || placesItself(up))
8879
9427
  return up;
8880
9428
  }
8881
9429
  return this.root;
@@ -9686,8 +10234,13 @@ export class Engine {
9686
10234
  * return fresh objects, so diffing their output would re-send every image and shadow on every
9687
10235
  * unrelated change.
9688
10236
  */
9689
- /** What a touch on an element that keeps a drag holds off, while it lasts: see `holdSwipe`. */
10237
+ /**
10238
+ * What a touch on an element that keeps a drag holds off, while it lasts (see `holdSwipe`), and
10239
+ * a node held unseen (see `hold`).
10240
+ */
9690
10241
  heldFor(node, merged) {
10242
+ if (node.held)
10243
+ Object.assign(merged, UNSEEN);
9691
10244
  if (node === this.swipeHeld)
9692
10245
  merged['gestureResponseDistance'] = NO_SWIPE;
9693
10246
  if (this.scrollHeld.size && this.scrollHeld.has(node))
@@ -9765,6 +10318,8 @@ export class Engine {
9765
10318
  if (field)
9766
10319
  fieldStyled(viewName, style, this.fontScale);
9767
10320
  const merged = composeTransform(node, this.eased(node, style));
10321
+ // From the sides as committed: the element's own, and each frame of a transition.
10322
+ settleUnsidedBorder(merged);
9768
10323
  // On iOS: Android's field says the baseline of the line it is sized and centred by.
9769
10324
  if (field)
9770
10325
  centreSingleLine(viewName, merged, this.fontScale, isOnBaseline(node, viewName));
@@ -9793,6 +10348,7 @@ export class Engine {
9793
10348
  // Asked of the namespace first: it is asked of every node merged.
9794
10349
  if (node.namespace !== undefined && viewName.startsWith('RNSVG')) {
9795
10350
  drawnAsShape(node, viewName, merged);
10351
+ this.fillUnread(merged);
9796
10352
  }
9797
10353
  if (node.raised !== undefined)
9798
10354
  merged['zIndex'] ??= node.raised;
@@ -9808,6 +10364,7 @@ export class Engine {
9808
10364
  if (merged['maxWidth'] !== undefined)
9809
10365
  leastWins(merged, 'Width');
9810
10366
  }
10367
+ keptForPaint(node, merged);
9811
10368
  return merged;
9812
10369
  }
9813
10370
  /** A run of text as it is committed: a word to a line while how narrow it goes is read. */
@@ -9962,11 +10519,17 @@ export class Engine {
9962
10519
  * an element is not seen here until the inline form compiles to one.
9963
10520
  */
9964
10521
  liftLines(node, flow) {
9965
- const runs = flow.plan.kind === 'lines'
10522
+ const all = flow.plan.kind === 'lines'
9966
10523
  ? [this.onLines(node)]
9967
10524
  : [...flow.plan.lines.values()].map((line) => line.boxes);
9968
- if (!runs.some((boxes) => boxes.length))
10525
+ // A line with nothing on it has no height, and its boxes are given no room: see
10526
+ // `addsNothing`.
10527
+ const runs = all.filter((boxes) => !boxes.every(addsNothing));
10528
+ if (!runs.length) {
10529
+ if (all.some((boxes) => boxes.length))
10530
+ nothingOnLines(flow);
9969
10531
  return;
10532
+ }
9970
10533
  const below = this.strutBelow(node);
9971
10534
  const lifts = new Map();
9972
10535
  const unders = new Map();
@@ -10565,6 +11128,10 @@ export class Engine {
10565
11128
  if (this.topOf(node) !== this.root)
10566
11129
  this.stopEased(node);
10567
11130
  }
11131
+ for (const node of [...this.paintedNodes]) {
11132
+ if (this.topOf(node) !== this.root)
11133
+ this.stopPainting(node);
11134
+ }
10568
11135
  }
10569
11136
  /** Every node whose animation native is playing, with no frame of it in JavaScript. */
10570
11137
  playedNatively = new Set();
@@ -10647,6 +11214,7 @@ export class Engine {
10647
11214
  return false;
10648
11215
  const length = Math.max(2, Math.round((spec.duration * span) / NATIVE_FRAME) + 1);
10649
11216
  const frames = Array.from({ length }, (_, i) => i / (length - 1));
11217
+ this.leavePainting(node);
10650
11218
  const native = this.scrollDriver.play(node, channels, { frames, toValue: span, iterations: count }, (view) => this.tagOf(view), () => this.endedNatively(node, running));
10651
11219
  if (!native)
10652
11220
  return false;
@@ -10700,6 +11268,21 @@ export class Engine {
10700
11268
  if (!running.done && running.pausedAt === undefined)
10701
11269
  this.playing.add(node);
10702
11270
  }
11271
+ /** Each animation started on a node held unseen (see `hold`), to be played once it is let go. */
11272
+ startsUnheld = new WeakSet();
11273
+ /**
11274
+ * Play an animation started on a node held unseen, once it is let go: its clock starts here, as
11275
+ * a browser starts it as it first paints the element. Not where it was paused meanwhile, nor a
11276
+ * second time where it was run again while held, which plays it here already.
11277
+ */
11278
+ playUnheld(node, current, spec, props) {
11279
+ if (!this.startsUnheld.delete(current) || spec.paused)
11280
+ return;
11281
+ current.start = this.now();
11282
+ if (current.pausedAt === undefined && !this.playing.has(node)) {
11283
+ this.playSomewhere(node, current, props);
11284
+ }
11285
+ }
10703
11286
  /** Start the clock on an animation, unless the node is already playing this one. */
10704
11287
  startPlaying(node, spec, frames, props) {
10705
11288
  const current = node.playing;
@@ -10734,6 +11317,7 @@ export class Engine {
10734
11317
  this.emitTransition(node, 'topAnimationstart', spec.name);
10735
11318
  return;
10736
11319
  }
11320
+ this.playUnheld(node, current, spec, props);
10737
11321
  // A view made again is another view: the one native was moving is gone. And one whose
10738
11322
  // important declarations changed is moving what it now must not, or not what it now may.
10739
11323
  if (node.committed === null || pinsChanged(node, current))
@@ -10746,7 +11330,11 @@ export class Engine {
10746
11330
  }
10747
11331
  /** Play an animation that has just started: by native's layout, by native's driver, or here. */
10748
11332
  playSomewhere(node, started, props) {
10749
- if (this.layPlayed(node, started))
11333
+ // A browser starts it as it first paints the element: a graph native plays would set its
11334
+ // opacity over the hold's. See `playUnheld`.
11335
+ if (node.held)
11336
+ this.startsUnheld.add(started);
11337
+ else if (this.layPlayed(node, started))
10750
11338
  this.laidPlayed.push({ node, running: started });
10751
11339
  else if (!this.playNatively(node, started, props))
10752
11340
  this.playing.add(node);
@@ -10771,8 +11359,7 @@ export class Engine {
10771
11359
  // One native was laying out goes on from JavaScript, by the clock it started on.
10772
11360
  const reframed = { ...current, tracks, inherited, laid: undefined };
10773
11361
  const { values, finished } = sample(reframed, current.pausedAt ?? this.now());
10774
- const holds = current.spec.fill === 'forwards' || current.spec.fill === 'both';
10775
- reframed.values = finished && !holds ? {} : values;
11362
+ reframed.values = finished && !fillsForwards(current) ? {} : values;
10776
11363
  this.playedFrames.set(reframed, frames);
10777
11364
  node.playing = reframed;
10778
11365
  // One that had nothing to paint was let go of: with frames that say something it plays on.
@@ -11218,8 +11805,13 @@ export class Engine {
11218
11805
  const clock = `${eased.duration} ${eased.easing.join()}`;
11219
11806
  clocks.set(clock, [...(clocks.get(clock) ?? []), key]);
11220
11807
  }
11221
- for (const keys of clocks.values())
11222
- this.playEased(node, state, keys, props);
11808
+ for (const keys of clocks.values()) {
11809
+ // Eased on the graph native already paints the view by, or from here: never on a second.
11810
+ if (node.paint)
11811
+ this.easePainted(node, state, keys, props);
11812
+ else
11813
+ this.playEased(node, state, keys, props);
11814
+ }
11223
11815
  }
11224
11816
  playEased(node, state, keys, props) {
11225
11817
  const first = state.get(keys[0]);
@@ -11254,6 +11846,7 @@ export class Engine {
11254
11846
  ],
11255
11847
  ];
11256
11848
  }));
11849
+ unsidedBorderTrack(tracks, props);
11257
11850
  const spec = {
11258
11851
  name: '',
11259
11852
  duration: first.duration,
@@ -11334,6 +11927,351 @@ export class Engine {
11334
11927
  clock.stop();
11335
11928
  this.forgetEased(node);
11336
11929
  }
11930
+ painter;
11931
+ /** The views native paints, for letting go of those that leave the tree. */
11932
+ paintedNodes = new Set();
11933
+ /** When each prop a view paints with last changed: see `paintedAgain`. */
11934
+ paintChanges = new WeakMap();
11935
+ quietTimer = null;
11936
+ /**
11937
+ * Give native the paint a commit would carry, where it is all that changed: a view whose
11938
+ * transform, opacity or background is changed again and again, a progress bar stepped every
11939
+ * 50ms, is painted by a native animated graph and leaves nothing to commit. Answers what is
11940
+ * left of `payload` to commit.
11941
+ *
11942
+ * The first change of a prop is committed as any other. The second within `PAINT_RUN_MS` is
11943
+ * committed too, with `collapsable: false` so the view is kept for native to paint, and from
11944
+ * the next native paints it. The shadow node falls behind what is drawn; it is brought up to
11945
+ * date with the next commit of the view for anything else, once the view has been left alone
11946
+ * for `PAINT_QUIET_MS`, and before anything measures where it is drawn.
11947
+ *
11948
+ * A view a graph of native's has set a transform or an opacity on before is left to
11949
+ * `setOwned`, which sets each later change of those on native: see `Touched`.
11950
+ */
11951
+ paintNatively(node, viewName, props, payload, cloned) {
11952
+ if (node.unsynced)
11953
+ return this.synced(node, props, payload);
11954
+ if (!payload && !node.paint?.sync)
11955
+ return payload;
11956
+ const held = this.heldOn(node);
11957
+ const runs = this.paintsHere(node, viewName);
11958
+ const state = node.paint;
11959
+ if (!state)
11960
+ return runs ? this.startPainting(node, props, payload) : payload;
11961
+ const { changed, inRun } = this.paintRun(node, state, payload, runs);
11962
+ if (changed)
11963
+ this.paintNow(node, state, props, held);
11964
+ else if (!state.sync)
11965
+ return payload;
11966
+ return this.leftOver(node, state, props, payload, held, cloned || !inRun);
11967
+ }
11968
+ /** The props other graphs of native's set on a view now. */
11969
+ heldOn(node) {
11970
+ return this.scrollDriver?.held(node) ?? NO_KEYS;
11971
+ }
11972
+ /**
11973
+ * What is left to commit of a payload native was given: nothing, where all it changed is
11974
+ * painted in a run, and otherwise all of it, with what native painted ahead of the shadow node.
11975
+ */
11976
+ leftOver(node, state, props, payload, held, commits) {
11977
+ const others = Object.keys(payload ?? {}).some((key) => !state.keys.has(key) || held.has(key));
11978
+ if (commits || others || state.sync)
11979
+ return this.withStale(state, props, payload);
11980
+ return this.leftToNative(node, state, props, payload);
11981
+ }
11982
+ /** Set on native what a view paints, of the props no other graph of native's holds. */
11983
+ paintNow(node, state, props, held) {
11984
+ const keys = held.size ? [...state.keys].filter((key) => !held.has(key)) : state.keys;
11985
+ const { values, refused } = this.paintValues(node, keys, (key) => this.paintedValue(node, key, props));
11986
+ // A colour native cannot take is committed: no graph owns a colour.
11987
+ for (const key of refused)
11988
+ state.keys.delete(key);
11989
+ state.shares = values.shares;
11990
+ if (!hasPaint(values))
11991
+ return;
11992
+ this.painter.paint(node, node.committed.tag, values);
11993
+ this.stats.paintedNatively++;
11994
+ }
11995
+ /**
11996
+ * Whether a run of changes can hand a view to native: a plain view no animation of native's
11997
+ * plays. A transition native plays is played on the painter's graph from then on, and a prop
11998
+ * another graph holds now is committed as before (`heldOn`).
11999
+ */
12000
+ paintsHere(node, viewName) {
12001
+ if (!this.painter || viewName !== DEFAULT_VIEW || node.committed === null)
12002
+ return false;
12003
+ return !node.playing?.native && !node.scrolled;
12004
+ }
12005
+ /** Have native paint a view, with no props yet: `paintRun` adds them. */
12006
+ paintFrom(node) {
12007
+ // One graph a view: the setter's, which `setOwned` connected, goes as the painter's comes.
12008
+ this.scrollDriver?.forget(node);
12009
+ const state = { keys: new Set(), stale: new Set() };
12010
+ node.paint = state;
12011
+ this.paintedNodes.add(node);
12012
+ return state;
12013
+ }
12014
+ /**
12015
+ * Whether a payload changes paint native could paint, and whether each such change is in a
12016
+ * run, where a run can hand the view over. Takes on each prop in a run, and each a graph of
12017
+ * native's has set on the view, which a commit no longer changes (see `Touched`).
12018
+ * A change in no run is committed as well as painted: a fade a press starts and a transition
12019
+ * that ends are a commit each, as they were, and the shadow node is never behind for them.
12020
+ */
12021
+ paintRun(node, state, payload, runs) {
12022
+ let changed = false;
12023
+ let inRun = runs;
12024
+ for (const key in payload) {
12025
+ if (!NATIVELY_EASED.has(key))
12026
+ continue;
12027
+ changed = true;
12028
+ const again = runs && this.paintedAgain(node, key);
12029
+ if (!again)
12030
+ inRun = false;
12031
+ if (again || this.owns(node, key))
12032
+ state.keys.add(key);
12033
+ }
12034
+ return { changed, inRun };
12035
+ }
12036
+ /**
12037
+ * Nothing to commit of a change native has painted: the view keeps its shadow node, and its
12038
+ * committed props are what the next change is measured from.
12039
+ */
12040
+ leftToNative(node, state, props, payload) {
12041
+ for (const key in payload)
12042
+ state.stale.add(key);
12043
+ node.committed = { ...node.committed, props };
12044
+ this.paintedInstead = true;
12045
+ this.quietLater();
12046
+ return null;
12047
+ }
12048
+ /** A payload with what native painted ahead of the shadow node, which it brings up to date. */
12049
+ withStale(state, props, payload) {
12050
+ state.sync = undefined;
12051
+ if (!state.stale.size)
12052
+ return payload;
12053
+ const out = { ...payload };
12054
+ for (const key of state.stale)
12055
+ if (!(key in out))
12056
+ out[key] = props[key] ?? null;
12057
+ state.stale.clear();
12058
+ return out;
12059
+ }
12060
+ /** Have native paint a view from the next change, where this one is the second of a run. */
12061
+ startPainting(node, props, payload) {
12062
+ const keys = [];
12063
+ for (const key in payload) {
12064
+ if (NATIVELY_EASED.has(key) && this.paintedAgain(node, key))
12065
+ keys.push(key);
12066
+ }
12067
+ if (!keys.length)
12068
+ return payload;
12069
+ const state = this.paintFrom(node);
12070
+ for (const key of keys)
12071
+ state.keys.add(key);
12072
+ if (props['collapsable'] !== undefined)
12073
+ return payload;
12074
+ props['collapsable'] = false;
12075
+ return { ...payload, collapsable: false };
12076
+ }
12077
+ /**
12078
+ * Whether a change of a prop a view paints with is the second of a run, soon after the last.
12079
+ * Two at the same moment are one: an animation's last frame and the style it rests at.
12080
+ */
12081
+ paintedAgain(node, key) {
12082
+ const now = this.now();
12083
+ const changes = this.paintChanges.get(node) ?? new Map();
12084
+ this.paintChanges.set(node, changes);
12085
+ const last = changes.get(key);
12086
+ if (last === now)
12087
+ return false;
12088
+ changes.set(key, now);
12089
+ return last !== undefined && now - last < PAINT_RUN_MS;
12090
+ }
12091
+ /** What a view paints for a key: where a transition native eases it is going, if it is. */
12092
+ paintedValue(node, key, props) {
12093
+ const eased = node.transitions?.get(key);
12094
+ return eased?.native ? eased.to : props[key];
12095
+ }
12096
+ /**
12097
+ * What native is to paint for these keys, and those it cannot take: a colour it cannot
12098
+ * parse. A transform the transform node cannot take, a matrix or a move by a share of a box
12099
+ * with no size yet, is painted whole (`PaintValues.transform`).
12100
+ */
12101
+ paintValues(node, keys, valueOf) {
12102
+ const values = {};
12103
+ const colors = {};
12104
+ const refused = [];
12105
+ for (const key of keys) {
12106
+ const value = valueOf(key) ?? null;
12107
+ if (key === 'opacity') {
12108
+ if (value !== null && typeof value !== 'number')
12109
+ refused.push(key);
12110
+ else
12111
+ values.opacity = this.touchedOpacity(node, value ?? 1);
12112
+ }
12113
+ else if (key === 'transform') {
12114
+ Object.assign(values, this.transformPainted(node, value));
12115
+ }
12116
+ else {
12117
+ const rgba = value === null ? [0, 0, 0, 0] : parseColor(value);
12118
+ if (rgba)
12119
+ colors[key] = rgba;
12120
+ else
12121
+ refused.push(key);
12122
+ }
12123
+ }
12124
+ values.colors = colors;
12125
+ return { values, refused };
12126
+ }
12127
+ /**
12128
+ * An opacity as native is to paint it: no lower than what still takes a touch, on a view that
12129
+ * takes presses, as a commit sends it (`stillTouched`).
12130
+ */
12131
+ touchedOpacity(node, opacity) {
12132
+ if (!this.responders.has(node))
12133
+ return opacity;
12134
+ return stillTouched({ opacity, pointerEvents: node.committed?.props['pointerEvents'] })['opacity'];
12135
+ }
12136
+ /** A transform as native is to paint it, and whether it is a share of the view's box. */
12137
+ transformPainted(node, value) {
12138
+ const list = value === null ? [] : paintedTransform(value, () => this.boxOf(node));
12139
+ if (!list)
12140
+ return { transform: { literal: literalTransform(value) } };
12141
+ return sharesBox(value) ? { transform: list, shares: true } : { transform: list };
12142
+ }
12143
+ /**
12144
+ * Paint again each view painted with a move by a share of its box, after a commit that can
12145
+ * have resized it: the points it was painted with are a share of the box it had.
12146
+ */
12147
+ repaintShares() {
12148
+ for (const node of this.paintedNodes) {
12149
+ const state = node.paint;
12150
+ if (!state.shares || !node.committed || !this.painter.paints(node))
12151
+ continue;
12152
+ if (node.transitions?.get('transform')?.native)
12153
+ continue;
12154
+ const held = this.heldOn(node);
12155
+ if (held.has('transform'))
12156
+ continue;
12157
+ const props = node.committed.props;
12158
+ const keys = [...state.keys].filter((key) => !held.has(key));
12159
+ const { values } = this.paintValues(node, keys, (key) => this.paintedValue(node, key, props));
12160
+ this.painter.paint(node, node.committed.tag, values);
12161
+ }
12162
+ }
12163
+ /**
12164
+ * Ease a transition on the graph native paints the view by, from where native has it: a
12165
+ * transition aimed somewhere else every 50ms is a value eased on native each time, with no
12166
+ * commit to start it. One native cannot ease this way is eased from JavaScript, and each of
12167
+ * its frames is painted.
12168
+ */
12169
+ easePainted(node, state, keys, props) {
12170
+ const tag = node.committed?.tag;
12171
+ const paint = node.paint;
12172
+ if (!this.painter?.eases || tag === undefined)
12173
+ return;
12174
+ if (INDIVIDUAL_TRANSFORMS.some((key) => props[key] !== undefined))
12175
+ return;
12176
+ // A prop the graph has none of builds it again, which stops what it eases: each of those is
12177
+ // taken back by JavaScript from where it has got to first, so none is left native's.
12178
+ if (keys.some((key) => !paint.keys.has(key)))
12179
+ this.backToScriptAll(node, keys);
12180
+ for (const key of keys)
12181
+ paint.keys.add(key);
12182
+ const ends = this.easedEnds(node, state, keys, props);
12183
+ if (!ends)
12184
+ return;
12185
+ const first = state.get(keys[0]);
12186
+ const length = Math.max(2, Math.round(first.duration / NATIVE_FRAME) + 1);
12187
+ const frames = Array.from({ length }, (_, i) => bezier(first.easing, i / (length - 1)));
12188
+ const clock = { keys, stop: () => undefined };
12189
+ for (const key of keys)
12190
+ state.get(key).native = clock;
12191
+ this.easedNatively.add(node);
12192
+ const eased = this.painter.ease(node, tag, ends.from, ends.to, frames, () => this.endedEased(node, clock));
12193
+ clock.stop = () => eased.stop();
12194
+ }
12195
+ /**
12196
+ * Where a transition on a view native paints starts and ends, as native eases them: null
12197
+ * where either is not numbers, which is eased from JavaScript and each frame painted.
12198
+ */
12199
+ easedEnds(node, state, keys, props) {
12200
+ const at = (end) => (key) => keys.includes(key) ? state.get(key)[end] : this.paintedValue(node, key, props);
12201
+ const ends = [at('current'), at('to')].map((valueOf) => this.paintValues(node, node.paint.keys, valueOf));
12202
+ const numbers = ends.every(({ values, refused }) => !refused.length && Array.isArray(values.transform ?? []));
12203
+ return numbers ? { from: ends[0].values, to: ends[1].values } : null;
12204
+ }
12205
+ /** Whether a graph of native's has set a prop on the view the node is committed as. */
12206
+ owns(node, key) {
12207
+ const owned = node.owned;
12208
+ return !!owned && owned.tag === node.committed?.tag && owned.keys.has(key);
12209
+ }
12210
+ /**
12211
+ * Let the painter go, for a graph of an animation to move the view: one graph moves a view,
12212
+ * and what the painter eased goes on from JavaScript.
12213
+ */
12214
+ leavePainting(node) {
12215
+ if (!node.paint)
12216
+ return;
12217
+ // What native painted that the shadow node has not got is committed with the view next.
12218
+ if (node.paint.stale.size)
12219
+ node.unsynced = new Set(node.paint.stale);
12220
+ this.backToScriptAll(node, []);
12221
+ this.stopPainting(node);
12222
+ }
12223
+ /** A payload with what native painted before the painter let the view go: see `leavePainting`. */
12224
+ synced(node, props, payload) {
12225
+ const out = { ...payload };
12226
+ for (const key of node.unsynced)
12227
+ if (!(key in out))
12228
+ out[key] = props[key] ?? null;
12229
+ node.unsynced = undefined;
12230
+ return out;
12231
+ }
12232
+ /** Take back from native every clock it eases on a view but those of `keys`. */
12233
+ backToScriptAll(node, keys) {
12234
+ const clocks = new Set();
12235
+ for (const [key, eased] of node.transitions ?? []) {
12236
+ if (eased.native && !keys.includes(key))
12237
+ clocks.add(eased.native);
12238
+ }
12239
+ for (const clock of clocks)
12240
+ this.backToScriptEased(node, clock, this.now());
12241
+ }
12242
+ /** Native no longer paints a view, which has left the tree. */
12243
+ stopPainting(node) {
12244
+ this.painter?.release(node);
12245
+ node.paint = undefined;
12246
+ this.paintedNodes.delete(node);
12247
+ }
12248
+ /** Bring every view native paints up to date in the shadow tree, once they are left alone. */
12249
+ quietLater() {
12250
+ if (this.quietTimer !== null)
12251
+ clearTimeout(this.quietTimer);
12252
+ this.quietTimer = setTimeout(() => {
12253
+ this.quietTimer = null;
12254
+ this.commitPainted();
12255
+ }, PAINT_QUIET_MS);
12256
+ }
12257
+ /**
12258
+ * Commit what native painted ahead of the shadow tree, on every view it paints: where a view
12259
+ * is drawn takes in the transform of each view its native view is in, which is not always
12260
+ * one of its parents here (a hoisted or fixed box is committed elsewhere).
12261
+ */
12262
+ commitPainted() {
12263
+ const behind = [];
12264
+ for (const each of this.paintedNodes) {
12265
+ if (!each.paint?.stale.size)
12266
+ continue;
12267
+ each.paint.sync = true;
12268
+ behind.push(each);
12269
+ }
12270
+ if (!behind.length)
12271
+ return;
12272
+ this.touch(behind);
12273
+ this.commit();
12274
+ }
11337
12275
  /** A graph of native's was connected to a view: see `Touched`. */
11338
12276
  touchedNatively(node, tag, keys) {
11339
12277
  const taken = keys.filter((key) => key === 'opacity' || key === 'transform');
@@ -11396,13 +12334,13 @@ export class Engine {
11396
12334
  clock.stop();
11397
12335
  }
11398
12336
  /**
11399
- * Whether an animation that has finished goes on holding its last frame. With a forwards fill
11400
- * it does, for good: without one the element falls back to whatever the cascade gives it, and
11401
- * 'backwards' fills the start only. Heard, it holds until whoever listens has been told: see
11402
- * `lettingGo`.
12337
+ * Whether an animation that has finished goes on holding its last frame. With a forwards fill,
12338
+ * or held there by script, it does, for good: without one the element falls back to whatever
12339
+ * the cascade gives it, and 'backwards' fills the start only. Heard, it holds until whoever
12340
+ * listens has been told: see `lettingGo`.
11403
12341
  */
11404
12342
  keeps(node, running) {
11405
- if (running.spec.fill === 'forwards' || running.spec.fill === 'both')
12343
+ if (fillsForwards(running))
11406
12344
  return true;
11407
12345
  // One that listened and has stopped takes its set with it: nobody to hear it.
11408
12346
  if (!node.listeners?.has('topAnimationend'))
@@ -11556,6 +12494,12 @@ export class Engine {
11556
12494
  }
11557
12495
  return out;
11558
12496
  }
12497
+ /** Copy a list of colours, each through the host's converter, one it cannot read transparent. */
12498
+ processColorList(value) {
12499
+ if (!Array.isArray(value))
12500
+ return value;
12501
+ return value.map((entry) => this.color(entry) ?? 0);
12502
+ }
11559
12503
  /** Copy a list of shadow-like objects, running each `color` through the host's converter. */
11560
12504
  processNestedColors(value) {
11561
12505
  if (!Array.isArray(value))
@@ -11693,6 +12637,8 @@ export class Engine {
11693
12637
  this.noteBaselineFields(node);
11694
12638
  this.freshBaselines(node);
11695
12639
  const childHandles = this.reconcileChildren(node, viewName, style);
12640
+ if (this.heldMoves.size)
12641
+ this.heldHandles(node, childHandles);
11696
12642
  if (this.fixedIn.size)
11697
12643
  this.fixedHandles(node, childHandles);
11698
12644
  this.raise(node, viewName);
@@ -11711,10 +12657,12 @@ export class Engine {
11711
12657
  // is here only because something beneath it moved has the props it had.
11712
12658
  const propsMoved = node.propsDirty || styleChanged;
11713
12659
  const props = propsMoved ? this.mergeProps(node, viewName) : previous.props;
11714
- const propsPayload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
12660
+ const payload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
11715
12661
  if (this.asked & LAYOUTS)
11716
- this.noteHow(node, propsPayload);
12662
+ this.noteHow(node, payload);
11717
12663
  const childrenChanged = this.childrenMoved(node, childHandles, previous.childHandles);
12664
+ // What native was given to paint is left out of the clone: see `paintNatively`.
12665
+ const propsPayload = this.paintNatively(node, viewName, props, payload, childrenChanged);
11718
12666
  if (this.notesMoves)
11719
12667
  this.noteClone(node, propsPayload, childrenChanged, childHandles, previous);
11720
12668
  let handle = previous.handle;
@@ -11820,7 +12768,7 @@ export class Engine {
11820
12768
  const payload = diffProps(previous, props);
11821
12769
  if (payload)
11822
12770
  borderColoursAgain(payload, props);
11823
- if (payload && node.owned)
12771
+ if (payload && node.owned && !node.paint)
11824
12772
  this.setOwned(node, payload, props);
11825
12773
  // A frame of a transform is committed with no layout settled after it: see `composeSticky`.
11826
12774
  if (payload && node.sticky?.drive && 'transform' in payload) {
@@ -12167,6 +13115,9 @@ export class Engine {
12167
13115
  * when it is shown again, as Modal.js creates it afresh. Committed again, it ignores every touch.
12168
13116
  */
12169
13117
  withheld(child) {
13118
+ // Moved with a finger on it, it is committed where it was: see `heldHandles`.
13119
+ if (this.heldMoves.size && this.heldElsewhere(child))
13120
+ return true;
12170
13121
  // A box drawn and then hidden is `display: none`, as any element is.
12171
13122
  if (this.generated !== null && isGenerated(child.tag)) {
12172
13123
  return this.undrawn(child) || this.undisplayed(child);
@@ -12577,6 +13528,24 @@ export class Engine {
12577
13528
  console.warn(`[angular-native] ${prop}: ${value}, in a style on <${on}>, is no value ${prop} takes:` +
12578
13529
  ` ${why}. The declaration is dropped, as a browser drops it.`);
12579
13530
  }
13531
+ platformColours = new Set();
13532
+ /**
13533
+ * Said once per token and props: a colour worked out of a token that holds a platform colour,
13534
+ * which has no channels to work with, is dropped. The compiler says so where one sheet holds
13535
+ * both; a token set in one sheet, or bound, and read in another is only known here.
13536
+ */
13537
+ reportPlatformColour(name, props, on) {
13538
+ const report = `${name} ${props.join(',')} ${on ?? ''}`;
13539
+ if (this.platformColours.has(report))
13540
+ return;
13541
+ this.platformColours.add(report);
13542
+ const where = on === undefined ? props.join(', ') : `${props.join(', ')}, bound on <${on}>,`;
13543
+ console.warn(`[angular-native] ${where} works a colour out of var(${name}), which holds a platform` +
13544
+ ` colour (platform-color() or a system colour). The platform resolves that by name and` +
13545
+ ` it has no channels to mix, fade or draw in a gradient or a shadow, so the declaration` +
13546
+ ` is dropped. Use var(${name}) on its own, where a colour property takes it whole, or a` +
13547
+ ` colour written out.`);
13548
+ }
12580
13549
  unreadDisplays = new Set();
12581
13550
  /**
12582
13551
  * Said once per token and value: the compiler refuses `display: grid` written out, but a token
@@ -12772,6 +13741,9 @@ export class Engine {
12772
13741
  * overlay would believe it and place itself in the corner.
12773
13742
  */
12774
13743
  measure(node, into) {
13744
+ // Where it is drawn takes in every transform above it, which the shadow nodes must have.
13745
+ if (this.paintedNodes.size)
13746
+ this.commitPainted();
12775
13747
  const handle = node.committed?.handle;
12776
13748
  if (handle)
12777
13749
  this.measureHandle(handle, into);
@@ -12827,6 +13799,15 @@ export class Engine {
12827
13799
  this.dropFocus();
12828
13800
  return this.focusedNode;
12829
13801
  }
13802
+ get focusedField() {
13803
+ const node = this.focused;
13804
+ return node && TEXT_INPUTS.has(viewNameOf(node)) ? node : null;
13805
+ }
13806
+ blurFocused() {
13807
+ const field = this.focusedField;
13808
+ if (field)
13809
+ this.dispatchCommand(field, 'blur');
13810
+ }
12830
13811
  /** Whether a node is still part of the tree, rather than in a subtree that was removed. */
12831
13812
  isAttached(node) {
12832
13813
  let current = node;
@@ -12841,6 +13822,17 @@ export class Engine {
12841
13822
  resolveAsset(value) {
12842
13823
  return this.resolveAssetSource(value);
12843
13824
  }
13825
+ /**
13826
+ * A drawing's fill that is no colour the host reads, `contrast` as Highcharts writes one for a
13827
+ * label, as the text's colour: native's own black is lost on a dark shape, and the text's is
13828
+ * seen. A stroke native cannot read is left to be dropped, as a browser drops it.
13829
+ */
13830
+ fillUnread(merged) {
13831
+ const fill = merged['fill'];
13832
+ if (typeof fill !== 'string' || fill === 'none' || this.color(fill) != null)
13833
+ return;
13834
+ merged['fill'] = CURRENT_BRUSH;
13835
+ }
12844
13836
  /** A colour prop as `processed` sends it: one dropped as unreadable is null on an update. */
12845
13837
  colourProp(key, value, update) {
12846
13838
  const painted = this.painted(key, value);
@@ -12852,6 +13844,8 @@ export class Engine {
12852
13844
  * reads no colour in no paint of the shape's own.
12853
13845
  */
12854
13846
  painted(key, value) {
13847
+ if (COLOR_LIST_PROPS.has(key))
13848
+ return this.processColorList(value);
12855
13849
  if (!BRUSH_PROPS.has(key))
12856
13850
  return this.color(value);
12857
13851
  // Already a brush, as the icon wrote it: only one the cascade settled is still text. An
@@ -12893,8 +13887,13 @@ export class Engine {
12893
13887
  path.push(current);
12894
13888
  return path.reverse();
12895
13889
  }
12896
- negotiate(target, event, phase) {
12897
- const path = this.pathTo(target);
13890
+ /**
13891
+ * Who asks for the touch: the nodes from the root down to `from`, which is the target, or for a
13892
+ * move while a node holds the touch, the lowest node above both it and the target, as React's
13893
+ * responder plugin walks. A node inside the responder never takes the touch back from it.
13894
+ */
13895
+ negotiate(target, event, phase, from = target) {
13896
+ const path = this.pathTo(from);
12898
13897
  const capture = `on${phase}ShouldSetResponderCapture`;
12899
13898
  const bubble = `on${phase}ShouldSetResponder`;
12900
13899
  // Capture runs root -> target, so an ancestor can claim before its children see it.
@@ -12965,22 +13964,29 @@ export class Engine {
12965
13964
  this.grant(elected, event);
12966
13965
  return;
12967
13966
  }
12968
- if (type === TOUCH_MOVE) {
12969
- if (this.currentResponder) {
12970
- this.responders.get(this.currentResponder)?.onResponderMove?.(event);
12971
- }
12972
- else {
12973
- const elected = this.negotiate(target, event, 'Move');
12974
- if (elected)
12975
- this.grant(elected, event);
12976
- }
12977
- return;
12978
- }
12979
- if (type === TOUCH_END)
13967
+ if (type === TOUCH_MOVE)
13968
+ this.moveResponder(target, event);
13969
+ else if (type === TOUCH_END)
12980
13970
  this.release(event, false);
12981
13971
  else if (type === TOUCH_CANCEL)
12982
13972
  this.release(event, true);
12983
13973
  }
13974
+ /**
13975
+ * Asked on every move, not only while nobody holds the touch: a scroll view or a list row's
13976
+ * swipe actions takes a drag from a pressable inside it this way. The responder answering for
13977
+ * itself keeps the touch, as in React. The move then goes to whoever holds the touch after the
13978
+ * asking, the new responder included.
13979
+ */
13980
+ moveResponder(target, event) {
13981
+ const holder = this.currentResponder;
13982
+ const from = holder ? commonAncestor(holder, target) : target;
13983
+ const elected = from ? this.negotiate(target, event, 'Move', from) : null;
13984
+ if (elected && elected !== holder)
13985
+ this.grant(elected, event);
13986
+ if (this.currentResponder) {
13987
+ this.responders.get(this.currentResponder)?.onResponderMove?.(event);
13988
+ }
13989
+ }
12984
13990
  // --- events -----------------------------------------------------------
12985
13991
  registerEventHandler() {
12986
13992
  if (this.eventsRegistered || !this.fabric.registerEventHandler)
@@ -13153,10 +14159,16 @@ export class Engine {
13153
14159
  * Stop every scroll view a node is in from scrolling, until the touch ends: one held off hands
13154
14160
  * the drag to the one around it. Telling native who holds the touch does this on Android. On
13155
14161
  * iOS a scroll view is held off only by a responder that is over it, never one inside it, so
13156
- * each is told not to scroll.
14162
+ * each is told not to scroll. The scroll views are the ones native has the node's view in,
14163
+ * which is where the finger is: an element moved with a finger on it stays there until the
14164
+ * finger lifts (see `holdMove`), and CDK's drag and drop moves the item it drags to the body in
14165
+ * the move whose default it prevents.
13157
14166
  */
13158
14167
  holdScroll(node) {
13159
- for (let up = node.parent; up; up = up.parent) {
14168
+ // Each once: a view's parent native has it in and one it is in since are both followed.
14169
+ const seen = new Set();
14170
+ for (let up = drawnIn(node); up && !seen.has(up); up = drawnIn(up)) {
14171
+ seen.add(up);
13160
14172
  if (up.nodeKind !== 'element' || !SCROLL_VIEWS.has(committedViewName(up)))
13161
14173
  continue;
13162
14174
  this.scrollHeld.add(up);