@ng-native/fabric 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css.d.ts +49 -2
- package/dist/css.js +206 -19
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +283 -11
- package/dist/engine.js +1068 -56
- package/dist/engine.js.map +1 -1
- package/dist/host.d.ts +8 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.js +18 -5
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +10 -4
- package/dist/inline-token.js +48 -5
- package/dist/inline-token.js.map +1 -1
- package/dist/native-paint.d.ts +66 -0
- package/dist/native-paint.js +246 -0
- package/dist/native-paint.js.map +1 -0
- package/dist/scroll-animation.d.ts +5 -0
- package/dist/scroll-animation.js +1 -1
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +5 -0
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
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
|
-
...
|
|
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
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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`,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 &&
|
|
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' ||
|
|
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
|
-
/**
|
|
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
|
|
10522
|
+
const all = flow.plan.kind === 'lines'
|
|
9966
10523
|
? [this.onLines(node)]
|
|
9967
10524
|
: [...flow.plan.lines.values()].map((line) => line.boxes);
|
|
9968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11401
|
-
* 'backwards' fills the start only. Heard, it holds until whoever
|
|
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
|
|
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
|
|
12660
|
+
const payload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
|
|
11715
12661
|
if (this.asked & LAYOUTS)
|
|
11716
|
-
this.noteHow(node,
|
|
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
|
-
|
|
12897
|
-
|
|
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
|
-
|
|
12970
|
-
|
|
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
|
-
|
|
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);
|