@ng-native/fabric 0.2.0 → 0.4.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/aria-props.d.ts +16 -0
- package/dist/aria-props.js +78 -0
- package/dist/aria-props.js.map +1 -0
- package/dist/color-mix.d.ts +12 -2
- package/dist/color-mix.js +16 -5
- package/dist/color-mix.js.map +1 -1
- package/dist/css.d.ts +160 -5
- package/dist/css.js +842 -87
- package/dist/css.js.map +1 -1
- package/dist/defer-triggers.js +3 -1
- package/dist/defer-triggers.js.map +1 -1
- package/dist/element-styles.d.ts +15 -0
- package/dist/element-styles.js +103 -0
- package/dist/element-styles.js.map +1 -0
- package/dist/engine.d.ts +289 -20
- package/dist/engine.js +1045 -120
- package/dist/engine.js.map +1 -1
- package/dist/font-faces.d.ts +5 -2
- package/dist/font-faces.js +13 -2
- package/dist/font-faces.js.map +1 -1
- package/dist/host.d.ts +7 -0
- package/dist/host.js +7 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-derived-token.d.ts +3 -12
- package/dist/inline-derived-token.js +249 -13
- package/dist/inline-derived-token.js.map +1 -1
- package/dist/inline-token.d.ts +12 -4
- package/dist/inline-token.js +76 -5
- package/dist/inline-token.js.map +1 -1
- package/dist/inline-transform.d.ts +5 -1
- package/dist/inline-transform.js +34 -6
- package/dist/inline-transform.js.map +1 -1
- package/dist/transition.d.ts +15 -4
- package/dist/transition.js +21 -3
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
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 { StyleResolver, } from "./css.js";
|
|
11
|
-
import {
|
|
10
|
+
import { boundDeclaration, inlineInherited, setsInherited, StyleResolver, } from "./css.js";
|
|
11
|
+
import { applyAria } from "./aria-props.js";
|
|
12
|
+
import { STYLED_ELEMENTS } from "./element-styles.js";
|
|
13
|
+
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
|
|
12
14
|
import { isFormsInput } from "./forms-inputs.js";
|
|
13
15
|
import { transformList } from "./inline-transform.js";
|
|
14
16
|
import { tokenFromValue } from "./inline-token.js";
|
|
@@ -22,14 +24,41 @@ import { FontFaces } from "./font-faces.js";
|
|
|
22
24
|
* and this runs for every node that commits.
|
|
23
25
|
*/
|
|
24
26
|
function animationOf(props) {
|
|
25
|
-
|
|
27
|
+
let spec = props['$animation'];
|
|
26
28
|
if (spec !== undefined)
|
|
27
29
|
delete props['$animation'];
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
for (const [key, field] of Object.entries(ANIMATION_PARTS)) {
|
|
31
|
+
if (!(key in props))
|
|
32
|
+
continue;
|
|
33
|
+
const value = props[key];
|
|
34
|
+
delete props[key];
|
|
35
|
+
if (spec && value !== null)
|
|
36
|
+
spec = { ...spec, ...animationPart(field, value) };
|
|
37
|
+
}
|
|
38
|
+
return spec;
|
|
39
|
+
}
|
|
40
|
+
const ANIMATION_PARTS = {
|
|
41
|
+
$animationDuration: 'duration',
|
|
42
|
+
$animationDelay: 'delay',
|
|
43
|
+
$animationEasing: 'easing',
|
|
44
|
+
$animationIterations: 'iterations',
|
|
45
|
+
$animationDirection: 'direction',
|
|
46
|
+
$animationFill: 'fill',
|
|
47
|
+
$playState: 'paused',
|
|
48
|
+
};
|
|
49
|
+
function animationPart(field, value) {
|
|
50
|
+
switch (field) {
|
|
51
|
+
case 'iterations':
|
|
52
|
+
return { iterations: value === 'infinite' ? null : value };
|
|
53
|
+
case 'direction':
|
|
54
|
+
return {
|
|
55
|
+
direction: value === 'normal' ? undefined : value,
|
|
56
|
+
};
|
|
57
|
+
case 'paused':
|
|
58
|
+
return { paused: value === 'paused' ? true : undefined };
|
|
59
|
+
default:
|
|
60
|
+
return { [field]: value };
|
|
61
|
+
}
|
|
33
62
|
}
|
|
34
63
|
/**
|
|
35
64
|
* Whether two specs ask for the same animation. By what they say rather than which object they
|
|
@@ -53,23 +82,24 @@ const SPEC_FIELDS = [
|
|
|
53
82
|
'direction',
|
|
54
83
|
'timeline',
|
|
55
84
|
];
|
|
85
|
+
/**
|
|
86
|
+
* The scroll events that mean a finger is dragging the list rather than tapping something in it.
|
|
87
|
+
*
|
|
88
|
+
* `topScrollBeginDrag` is the precise one; `topScroll` is here because a scroll view configured
|
|
89
|
+
* without drag events still reports movement, and a press surviving that is the bug either way.
|
|
90
|
+
*/
|
|
91
|
+
const SCROLL_CANCELS_PRESS = new Set(['topScroll', 'topScrollBeginDrag']);
|
|
56
92
|
/**
|
|
57
93
|
* RN's `directEventTypes`: delivered to the target only, never up the tree. Everything else
|
|
58
94
|
* bubbles, which matches RN's `bubblingEventTypes` for the touch and input events that matter.
|
|
95
|
+
* The web host's engine reads this set too, so both hosts deliver an event the same way.
|
|
59
96
|
*
|
|
60
97
|
* ponytail: a hand-kept set rather than real ViewConfigs. It is wrong only for a nested
|
|
61
98
|
* scroll view, where an ancestor listening for the same direct event would not fire (correct)
|
|
62
99
|
* but a third-party component's custom direct event would bubble (harmless in practice).
|
|
63
100
|
* Swap it for per-component ViewConfig metadata if that ever matters.
|
|
64
101
|
*/
|
|
65
|
-
|
|
66
|
-
* The scroll events that mean a finger is dragging the list rather than tapping something in it.
|
|
67
|
-
*
|
|
68
|
-
* `topScrollBeginDrag` is the precise one; `topScroll` is here because a scroll view configured
|
|
69
|
-
* without drag events still reports movement, and a press surviving that is the bug either way.
|
|
70
|
-
*/
|
|
71
|
-
const SCROLL_CANCELS_PRESS = new Set(['topScroll', 'topScrollBeginDrag']);
|
|
72
|
-
const DIRECT_EVENTS = new Set([
|
|
102
|
+
export const DIRECT_EVENTS = new Set([
|
|
73
103
|
'topScroll',
|
|
74
104
|
'topScrollBeginDrag',
|
|
75
105
|
'topScrollEndDrag',
|
|
@@ -108,9 +138,11 @@ const DIRECT_EVENTS = new Set([
|
|
|
108
138
|
*/
|
|
109
139
|
export class SyntheticEvent {
|
|
110
140
|
nativeEvent;
|
|
141
|
+
target;
|
|
111
142
|
stopped = false;
|
|
112
|
-
constructor(nativeEvent) {
|
|
143
|
+
constructor(nativeEvent, target = null) {
|
|
113
144
|
this.nativeEvent = nativeEvent;
|
|
145
|
+
this.target = target;
|
|
114
146
|
}
|
|
115
147
|
stopPropagation() {
|
|
116
148
|
this.stopped = true;
|
|
@@ -210,6 +242,34 @@ const VIEW_NAMES = {
|
|
|
210
242
|
* `@ng-native/expo`, and warning about it would be noise.
|
|
211
243
|
*/
|
|
212
244
|
const PRIMITIVE_NAMES = new Set(Object.keys(VIEW_NAMES));
|
|
245
|
+
/**
|
|
246
|
+
* HTML's text elements, which a template can write as it would for a browser.
|
|
247
|
+
*
|
|
248
|
+
* Each is text when it holds only text and other text elements, and a view when it holds anything
|
|
249
|
+
* else: a box inside a paragraph has no layout, so `<span>Inbox<view class="dot" /></span>` is a
|
|
250
|
+
* view with its text in a paragraph of its own. Decided as the element is committed, by
|
|
251
|
+
* `viewNameOf`. They have no component to import, so they are not `PRIMITIVE_NAMES`.
|
|
252
|
+
*/
|
|
253
|
+
const TEXT_ELEMENTS = new Set([
|
|
254
|
+
...['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'strong', 'b', 'em', 'i'],
|
|
255
|
+
...['u', 's', 'small', 'code', 'mark', 'abbr', 'cite', 'time'],
|
|
256
|
+
]);
|
|
257
|
+
/** HTML's layout elements, each a plain view, as a browser's are boxes with a name. */
|
|
258
|
+
const LAYOUT_ELEMENTS = [
|
|
259
|
+
...['div', 'section', 'article', 'header', 'footer', 'main', 'nav', 'ul', 'ol', 'li'],
|
|
260
|
+
];
|
|
261
|
+
for (const name of TEXT_ELEMENTS)
|
|
262
|
+
VIEW_NAMES[name] = 'Paragraph';
|
|
263
|
+
for (const name of LAYOUT_ELEMENTS)
|
|
264
|
+
VIEW_NAMES[name] = 'View';
|
|
265
|
+
/**
|
|
266
|
+
* What an HTML element has for its name alone, given to the node as it is made: a heading's role,
|
|
267
|
+
* which a binding can replace as it can any prop, and whether the engine has styles for it.
|
|
268
|
+
*/
|
|
269
|
+
const HTML_ELEMENTS = new Map([
|
|
270
|
+
...['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].map((name) => [name, { role: 'header' }]),
|
|
271
|
+
...[...STYLED_ELEMENTS].map((name) => [name, { styled: true }]),
|
|
272
|
+
]);
|
|
213
273
|
/**
|
|
214
274
|
* The props each host primitive's native view is sent, by element name, as the components
|
|
215
275
|
* declare them: every input the element's component has, and the props it writes that no input
|
|
@@ -244,6 +304,14 @@ export function claimHost(node) {
|
|
|
244
304
|
export function markComponentHost(node) {
|
|
245
305
|
node.componentHost = true;
|
|
246
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Say that a component's host is the native view its element name is registered as: a typed
|
|
309
|
+
* wrapper for the view, where a component of the app's own with the same selector is a plain view.
|
|
310
|
+
* Called from the component's constructor. See `ViewNameOptions.yieldsToComponents`.
|
|
311
|
+
*/
|
|
312
|
+
export function keepNativeView(node) {
|
|
313
|
+
node.nativeView = true;
|
|
314
|
+
}
|
|
247
315
|
/** `text-input` -> `TextInput`: how the components package names the class for an element. */
|
|
248
316
|
const className = (element) => element.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
249
317
|
/** Names Android registers differently. */
|
|
@@ -256,6 +324,11 @@ export const ANDROID_VIEW_NAMES = {
|
|
|
256
324
|
'safe-area-view': 'View',
|
|
257
325
|
'input-accessory-view': 'View',
|
|
258
326
|
};
|
|
327
|
+
/**
|
|
328
|
+
* iOS's names for the elements Android names differently, so reporting iOS after Android puts
|
|
329
|
+
* them back.
|
|
330
|
+
*/
|
|
331
|
+
const IOS_VIEW_NAMES = Object.fromEntries(Object.keys(ANDROID_VIEW_NAMES).map((element) => [element, VIEW_NAMES[element]]));
|
|
259
332
|
/**
|
|
260
333
|
* The platform the host reported at startup. Defaults to iOS, which is what the view-name table
|
|
261
334
|
* above assumes too.
|
|
@@ -274,11 +347,9 @@ export function nativePlatform() {
|
|
|
274
347
|
*/
|
|
275
348
|
export function registerPlatformComponents(platform) {
|
|
276
349
|
platformOS = platform;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
for (const [element, viewName] of Object.entries(ANDROID_VIEW_NAMES)) {
|
|
350
|
+
const names = platform === 'android' ? ANDROID_VIEW_NAMES : IOS_VIEW_NAMES;
|
|
351
|
+
for (const [element, viewName] of Object.entries(names))
|
|
280
352
|
registerViewName(element, viewName);
|
|
281
|
-
}
|
|
282
353
|
}
|
|
283
354
|
/**
|
|
284
355
|
* Props RN's JS wrappers apply before the caller's, per native view name.
|
|
@@ -318,6 +389,88 @@ const DEFAULT_VIEW = 'View';
|
|
|
318
389
|
/** A top-level text, the one view that aligns its lines. A nested text is a `VirtualText`. */
|
|
319
390
|
const PARAGRAPH = 'Paragraph';
|
|
320
391
|
const textDirection = (value) => value === 'ltr' || value === 'rtl' ? value : undefined;
|
|
392
|
+
/**
|
|
393
|
+
* What iOS scales a text's `lineHeight` by: the system text size, capped by the text's own
|
|
394
|
+
* `maxFontSizeMultiplier` where it is at least 1, and 1 without font scaling
|
|
395
|
+
* (`RCTEffectiveFontSizeMultiplierFromTextAttributes`).
|
|
396
|
+
*/
|
|
397
|
+
function textScale(props, fontScale = 1) {
|
|
398
|
+
if (props['allowFontScaling'] === false)
|
|
399
|
+
return 1;
|
|
400
|
+
const cap = props['maxFontSizeMultiplier'];
|
|
401
|
+
return typeof cap === 'number' && cap >= 1 ? Math.min(fontScale, cap) : fontScale;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Centre the text of a single-line text field that has a line height, as Chrome centres an
|
|
405
|
+
* input's, keeping the height the line height gives it.
|
|
406
|
+
*
|
|
407
|
+
* Where a `height` in points sizes the field, the line height has nothing left to do, and is left
|
|
408
|
+
* out on both platforms: on Android, `EditText` centres a line box taller than the font's own
|
|
409
|
+
* about 1.7pt high of centre in a 44pt field, and centres the font's own exactly. A percentage
|
|
410
|
+
* height sizes the field only where its parent's height is definite, which is known at layout
|
|
411
|
+
* alone, so such a field is left as it was. Elsewhere Android sizes the field by the line height
|
|
412
|
+
* and centres the text in it itself, and keeps it.
|
|
413
|
+
*
|
|
414
|
+
* On iOS, React Native's field sets `lineHeight` as the paragraph's minimum and maximum line
|
|
415
|
+
* height and, unlike a paragraph (`RCTApplyBaselineOffset`), never offsets the baseline, so the
|
|
416
|
+
* glyphs sit at the bottom of a line box taller than the font. One line has nothing to space, so
|
|
417
|
+
* the line height is left out. What it does in Chrome, and on Android, is set the field's height:
|
|
418
|
+
* line height, padding and border. That is kept as a `minHeight`, the larger of it and the field's
|
|
419
|
+
* own, within its `maxHeight`: the whole sum for a border-box field, Yoga's default, and the line
|
|
420
|
+
* height alone for a content-box one. A null `height` is no height. A value that is not a number
|
|
421
|
+
* cannot be added up, and leaves the field as it was.
|
|
422
|
+
*/
|
|
423
|
+
function centreSingleLine(viewName, props, fontScale) {
|
|
424
|
+
const lineHeight = props['lineHeight'];
|
|
425
|
+
if (!TEXT_INPUTS.has(viewName) || typeof lineHeight !== 'number')
|
|
426
|
+
return;
|
|
427
|
+
if (props['multiline'] === true)
|
|
428
|
+
return;
|
|
429
|
+
const height = props['height'];
|
|
430
|
+
if (typeof height === 'number') {
|
|
431
|
+
delete props['lineHeight'];
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// A percentage is a fixed height only where the parent's is definite, known at layout alone.
|
|
435
|
+
if (isSet(height) && height !== 'auto')
|
|
436
|
+
return;
|
|
437
|
+
// Android sizes the field by its line height, and centres the text in it, itself.
|
|
438
|
+
if (viewName !== 'TextInput')
|
|
439
|
+
return;
|
|
440
|
+
const edge = (side, logical) => [
|
|
441
|
+
props[`padding${side}`] ??
|
|
442
|
+
props[`paddingBlock${logical}`] ??
|
|
443
|
+
props['paddingBlock'] ??
|
|
444
|
+
props['paddingVertical'] ??
|
|
445
|
+
props['padding'] ??
|
|
446
|
+
0,
|
|
447
|
+
props[`border${side}Width`] ?? props['borderWidth'] ?? 0,
|
|
448
|
+
];
|
|
449
|
+
const line = lineHeight * textScale(props, fontScale);
|
|
450
|
+
const box = [line, ...edge('Top', 'Start'), ...edge('Bottom', 'End')];
|
|
451
|
+
const own = props['minHeight'] ?? 0;
|
|
452
|
+
const max = props['maxHeight'] ?? Infinity;
|
|
453
|
+
if (![...box, own, max].every((part) => typeof part === 'number'))
|
|
454
|
+
return;
|
|
455
|
+
delete props['lineHeight'];
|
|
456
|
+
// A content-box field's minHeight is its content's: Yoga adds the padding and border itself.
|
|
457
|
+
const content = props['boxSizing'] === 'content-box'
|
|
458
|
+
? line
|
|
459
|
+
: box.reduce((sum, part) => sum + part, 0);
|
|
460
|
+
props['minHeight'] = Math.max(own, Math.min(content, max));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Start a multiline Android text field's text at the top, as iOS and a `<textarea>` do.
|
|
464
|
+
*
|
|
465
|
+
* React Native maps an unset `textAlignVertical` to `Gravity.NO_GRAVITY`, which `ReactEditText`
|
|
466
|
+
* reads as the `EditText`'s own gravity, `center_vertical`. One the app set, through the input or
|
|
467
|
+
* a CSS `vertical-align`, stays.
|
|
468
|
+
*/
|
|
469
|
+
function alignMultiline(viewName, props) {
|
|
470
|
+
if (viewName !== 'AndroidTextInput' || props['multiline'] !== true)
|
|
471
|
+
return;
|
|
472
|
+
props['textAlignVertical'] ??= 'top';
|
|
473
|
+
}
|
|
321
474
|
/**
|
|
322
475
|
* Resolve a paragraph's `text-align` against its direction, as CSS does.
|
|
323
476
|
*
|
|
@@ -361,13 +514,61 @@ const MODAL_HOST = 'ModalHostView';
|
|
|
361
514
|
* and skipping it is not cosmetic: `RNSScreenStackHeaderConfig` without its `position: absolute`
|
|
362
515
|
* joins the screen's flex layout and pushes every page down by the height of the header.
|
|
363
516
|
*/
|
|
364
|
-
export function registerViewName(elementName, viewName, defaultProps) {
|
|
517
|
+
export function registerViewName(elementName, viewName, defaultProps, options) {
|
|
365
518
|
VIEW_NAMES[elementName] = viewName;
|
|
366
|
-
if (
|
|
367
|
-
|
|
519
|
+
if (options?.yieldsToComponents)
|
|
520
|
+
YIELDING.add(elementName);
|
|
521
|
+
else
|
|
522
|
+
YIELDING.delete(elementName);
|
|
523
|
+
// A name the app registers is the app's: no longer decided by what the element holds.
|
|
524
|
+
TEXT_ELEMENTS.delete(elementName);
|
|
368
525
|
for (const name of typeof viewName === 'string' ? [viewName] : [viewName.ios, viewName.android]) {
|
|
369
|
-
|
|
526
|
+
if (defaultProps)
|
|
527
|
+
DEFAULT_PROPS[name] = defaultProps;
|
|
528
|
+
if (options?.textContent)
|
|
529
|
+
TEXT_CONTENT_PROPS[name] = options.textContent;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** The registered names that give way to a component's own host. See `ViewNameOptions`. */
|
|
533
|
+
const YIELDING = new Set();
|
|
534
|
+
/** The prop each view that takes its text content as a prop reads it from. See `ViewNameOptions`. */
|
|
535
|
+
const TEXT_CONTENT_PROPS = {};
|
|
536
|
+
/** Whether `node` takes its text content as a prop rather than as text children. */
|
|
537
|
+
function takesTextAsProp(node) {
|
|
538
|
+
return node.kind === 'element' && TEXT_CONTENT_PROPS[viewNameOf(node)] !== undefined;
|
|
539
|
+
}
|
|
540
|
+
/** Sets a view's text prop from its content, unless the prop is set explicitly. */
|
|
541
|
+
function withTextContent(node, viewName, props) {
|
|
542
|
+
const prop = TEXT_CONTENT_PROPS[viewName];
|
|
543
|
+
if (!prop || node.props[prop] !== undefined)
|
|
544
|
+
return;
|
|
545
|
+
const text = textContent(node);
|
|
546
|
+
if (text)
|
|
547
|
+
props[prop] = text;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* The whitespace a paragraph drops at its ends is what CSS collapses: spaces, tabs and line
|
|
551
|
+
* breaks. A no-break space is text, as in a browser, so ` ` is how a value keeps a space at
|
|
552
|
+
* its end; `String.prototype.trim` would take that too.
|
|
553
|
+
*/
|
|
554
|
+
const trimStart = (text) => text.replace(/^[ \t\n\r\f]+/, '');
|
|
555
|
+
const trimEnd = (text) => text.replace(/[ \t\n\r\f]+$/, '');
|
|
556
|
+
const trimEnds = (text) => trimEnd(trimStart(text));
|
|
557
|
+
/**
|
|
558
|
+
* The text written inside a view that takes it as a prop: every run joined, with the whitespace at
|
|
559
|
+
* the two ends dropped as a paragraph drops it, and kept inside. A nested view is drawn after the
|
|
560
|
+
* text, as SwiftUI's `Text` through `@expo/ui` draws a child `Text`, so the space before it stays.
|
|
561
|
+
*/
|
|
562
|
+
function textContent(node) {
|
|
563
|
+
let text = '';
|
|
564
|
+
let nested = false;
|
|
565
|
+
for (const child of node.children) {
|
|
566
|
+
if (child.kind === 'text')
|
|
567
|
+
text += child.text;
|
|
568
|
+
else if (child.kind === 'element')
|
|
569
|
+
nested = true;
|
|
370
570
|
}
|
|
571
|
+
return nested ? trimStart(text) : trimEnd(trimStart(text));
|
|
371
572
|
}
|
|
372
573
|
/** An element's registered view name, for the platform the host reported. */
|
|
373
574
|
function registeredViewName(elementName) {
|
|
@@ -381,6 +582,17 @@ function registeredViewName(elementName) {
|
|
|
381
582
|
* a paragraph, and a text input on either platform. See `Engine.remeasureText`.
|
|
382
583
|
*/
|
|
383
584
|
const MEASURED_VIEWS = new Set(['Paragraph', 'TextInput', 'AndroidTextInput']);
|
|
585
|
+
/** Leave a family that is still loading out of `style`, remembering it. See `loadingFamilies`. */
|
|
586
|
+
function holdLoadingFamily(node, style) {
|
|
587
|
+
const family = style['fontFamily'];
|
|
588
|
+
if (typeof family === 'string' && loadingFamilies.has(family)) {
|
|
589
|
+
delete style['fontFamily'];
|
|
590
|
+
node.heldFamily = family;
|
|
591
|
+
}
|
|
592
|
+
else if (node.heldFamily !== undefined) {
|
|
593
|
+
node.heldFamily = undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
384
596
|
/** What each mounted app does when faces register. See `fontsRegistered`. */
|
|
385
597
|
const fontListeners = new Set();
|
|
386
598
|
/** Hear about faces registering with the platform, until the returned function is called. */
|
|
@@ -400,6 +612,45 @@ export function fontsRegistered(families) {
|
|
|
400
612
|
for (const listener of [...fontListeners])
|
|
401
613
|
listener(names);
|
|
402
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Families a load has asked the platform for and not yet settled, by how many loads asked. Text
|
|
617
|
+
* naming one is laid out in the fallback without the name until it settles: native caches a
|
|
618
|
+
* text's measurement by its family name, so a fallback measurement under the face's name would
|
|
619
|
+
* outlast the face arriving.
|
|
620
|
+
*/
|
|
621
|
+
const loadingFamilies = new Map();
|
|
622
|
+
/** Faces a load is about to ask the platform for. See `loadingFamilies`. */
|
|
623
|
+
export function fontsLoading(families) {
|
|
624
|
+
for (const family of families)
|
|
625
|
+
loadingFamilies.set(family, (loadingFamilies.get(family) ?? 0) + 1);
|
|
626
|
+
}
|
|
627
|
+
/** What each mounted app does when a load settles. See `fontsSettled`. */
|
|
628
|
+
const settleListeners = new Set();
|
|
629
|
+
/** Hear about loads settling, until the returned function is called. */
|
|
630
|
+
export function onFontsSettled(listener) {
|
|
631
|
+
settleListeners.add(listener);
|
|
632
|
+
return () => settleListeners.delete(listener);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Faces a load asked for and has finished with, registered or not, before `fontsRegistered`.
|
|
636
|
+
* Text held back for one that no load still asks for is laid out with the name again: see
|
|
637
|
+
* `Engine.fontsSettled`.
|
|
638
|
+
*/
|
|
639
|
+
export function fontsSettled(families) {
|
|
640
|
+
const settled = new Set();
|
|
641
|
+
for (const family of families) {
|
|
642
|
+
const left = (loadingFamilies.get(family) ?? 1) - 1;
|
|
643
|
+
if (left > 0)
|
|
644
|
+
loadingFamilies.set(family, left);
|
|
645
|
+
else {
|
|
646
|
+
loadingFamilies.delete(family);
|
|
647
|
+
settled.add(family);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (settled.size)
|
|
651
|
+
for (const listener of [...settleListeners])
|
|
652
|
+
listener(settled);
|
|
653
|
+
}
|
|
403
654
|
/** Whether a paragraph, or a span in it, was committed asking for one of these families. */
|
|
404
655
|
function namesFamily(node, families) {
|
|
405
656
|
const family = node.committed?.props['fontFamily'];
|
|
@@ -470,12 +721,13 @@ const STAND_INS = {};
|
|
|
470
721
|
*/
|
|
471
722
|
function paragraphText(node) {
|
|
472
723
|
let root = node.parent;
|
|
473
|
-
|
|
474
|
-
|
|
724
|
+
// Text written straight into a view is a paragraph of its own, and all of it.
|
|
725
|
+
if (root === null || !isTextElement(root))
|
|
726
|
+
return node.box ? trimEnds(node.text) : node.text;
|
|
475
727
|
// The commonest paragraph by far: one run and nothing else, so it is both the first and last.
|
|
476
|
-
if (root.children.length === 1 && root.parent
|
|
477
|
-
return node.text
|
|
478
|
-
while (root.parent
|
|
728
|
+
if (root.children.length === 1 && !isTextElement(root.parent))
|
|
729
|
+
return trimEnds(node.text);
|
|
730
|
+
while (isTextElement(root.parent))
|
|
479
731
|
root = root.parent;
|
|
480
732
|
const runs = [];
|
|
481
733
|
const collect = (from) => {
|
|
@@ -484,18 +736,39 @@ function paragraphText(node) {
|
|
|
484
736
|
if (child.text)
|
|
485
737
|
runs.push(child);
|
|
486
738
|
}
|
|
487
|
-
else if (child
|
|
739
|
+
else if (isTextElement(child))
|
|
488
740
|
collect(child);
|
|
489
741
|
}
|
|
490
742
|
};
|
|
491
743
|
collect(root);
|
|
492
744
|
let text = node.text;
|
|
493
745
|
if (runs[0] === node)
|
|
494
|
-
text = text
|
|
746
|
+
text = trimStart(text);
|
|
495
747
|
if (runs[runs.length - 1] === node)
|
|
496
|
-
text = text
|
|
748
|
+
text = trimEnd(text);
|
|
497
749
|
return text;
|
|
498
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* Whether an element is text: a `<text>`, or one of HTML's text elements that holds only text and
|
|
753
|
+
* other such elements. Worked out each time, from the tree as it is, so nothing can go stale.
|
|
754
|
+
*/
|
|
755
|
+
function isTextElement(node) {
|
|
756
|
+
if (node === null || node.kind !== 'element')
|
|
757
|
+
return false;
|
|
758
|
+
if (node.name === 'text')
|
|
759
|
+
return true;
|
|
760
|
+
// One lookup settles nearly every other element, which is a view.
|
|
761
|
+
if (registeredViewName(node.name) !== PARAGRAPH)
|
|
762
|
+
return false;
|
|
763
|
+
return !TEXT_ELEMENTS.has(node.name) || holdsOnlyText(node);
|
|
764
|
+
}
|
|
765
|
+
function holdsOnlyText(node) {
|
|
766
|
+
for (const child of node.children ?? []) {
|
|
767
|
+
if (child.kind === 'element' && !isTextElement(child))
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
499
772
|
/**
|
|
500
773
|
* The native view a node commits as: `Paragraph` for a `<text>`, `VirtualText` for one nested in
|
|
501
774
|
* another, `RawText` for a run of text, and `View` for anything unregistered, which is what an
|
|
@@ -511,9 +784,169 @@ export function viewNameOf(node) {
|
|
|
511
784
|
// RN models nested text as a span inside a paragraph: `Paragraph > [RawText, Text > RawText]`.
|
|
512
785
|
// Committing a nested `<text>` as another Paragraph gives a paragraph inside a paragraph,
|
|
513
786
|
// which lays out as a separate block instead of flowing inline.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
787
|
+
// A component of the app's own under a name a view set registered: its host is a plain view.
|
|
788
|
+
if (node.componentHost && !node.nativeView && YIELDING.has(node.name))
|
|
789
|
+
return DEFAULT_VIEW;
|
|
790
|
+
const registered = registeredViewName(node.name);
|
|
791
|
+
// Nearly every element: a view of some kind, by its name alone.
|
|
792
|
+
if (registered !== PARAGRAPH)
|
|
793
|
+
return registered ?? DEFAULT_VIEW;
|
|
794
|
+
// One of HTML's text elements that holds something other than text: a view.
|
|
795
|
+
if (node.name !== 'text' && TEXT_ELEMENTS.has(node.name) && !holdsOnlyText(node)) {
|
|
796
|
+
return DEFAULT_VIEW;
|
|
797
|
+
}
|
|
798
|
+
return isTextElement(node.parent) ? VIRTUAL_TEXT : PARAGRAPH;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* The props every native view reads as a boolean: the `bool` fields of React Native's
|
|
802
|
+
* `BaseViewProps`, `AccessibilityProps` and `HostPlatformViewProps`, by the names JavaScript sends.
|
|
803
|
+
*
|
|
804
|
+
* An attribute arrives as text. A component that declares the prop as an input turns it into a
|
|
805
|
+
* boolean itself, but an element nothing claims, such as a component's own host, holds what it
|
|
806
|
+
* was given, and Android refuses a string where it reads a boolean: `focusable="false"` is
|
|
807
|
+
* `java.lang.String cannot be cast to java.lang.Boolean` at the first commit. iOS ignores it.
|
|
808
|
+
*
|
|
809
|
+
* So the text is made a boolean as it is committed, `"false"` false and anything else true, as an
|
|
810
|
+
* attribute reads in HTML. The node keeps the text, which is what `[focusable="false"]` in a
|
|
811
|
+
* stylesheet matches against.
|
|
812
|
+
*/
|
|
813
|
+
const BOOLEAN_VIEW_PROPS = new Set([
|
|
814
|
+
'accessibilityElementsHidden',
|
|
815
|
+
'accessibilityIgnoresInvertColors',
|
|
816
|
+
'accessibilityRespondsToUserInteraction',
|
|
817
|
+
'accessibilityShowsLargeContentViewer',
|
|
818
|
+
'accessibilityViewIsModal',
|
|
819
|
+
'accessible',
|
|
820
|
+
'collapsable',
|
|
821
|
+
'collapsableChildren',
|
|
822
|
+
'focusable',
|
|
823
|
+
'hasTVPreferredFocus',
|
|
824
|
+
'needsOffscreenAlphaCompositing',
|
|
825
|
+
// The flags native reads to know an event has a listener, which are booleans as well.
|
|
826
|
+
'onAccessibilityAction',
|
|
827
|
+
'onAccessibilityEscape',
|
|
828
|
+
'onAccessibilityMagicTap',
|
|
829
|
+
'onAccessibilityTap',
|
|
830
|
+
'onLayout',
|
|
831
|
+
'removeClippedSubviews',
|
|
832
|
+
'renderToHardwareTextureAndroid',
|
|
833
|
+
'screenReaderFocusable',
|
|
834
|
+
'shouldRasterizeIOS',
|
|
835
|
+
]);
|
|
836
|
+
/**
|
|
837
|
+
* Whether `node` holds nothing `:empty` sees besides `moved`: so it was empty before `moved` came,
|
|
838
|
+
* or is now that it has gone. An anchor is a comment on the web, which `:empty` does not see.
|
|
839
|
+
*/
|
|
840
|
+
function wasOrIsEmptyWithout(node, moved) {
|
|
841
|
+
if (moved.kind === 'anchor')
|
|
842
|
+
return false;
|
|
843
|
+
for (const child of node.children) {
|
|
844
|
+
if (child !== moved && child.kind !== 'anchor')
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
|
|
851
|
+
* attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
|
|
852
|
+
* native reads.
|
|
853
|
+
*/
|
|
854
|
+
function writeOwnProps(own, props) {
|
|
855
|
+
let aria = false;
|
|
856
|
+
for (const key of Object.keys(own)) {
|
|
857
|
+
if (key.includes('-'))
|
|
858
|
+
aria ||= key.startsWith('aria-');
|
|
859
|
+
else if (key !== 'style' && key !== INTRINSIC_SIZE && key !== STYLE_OVERRIDE) {
|
|
860
|
+
props[key] = committedProp(key, own[key]);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (aria)
|
|
864
|
+
applyAria(own, props);
|
|
865
|
+
}
|
|
866
|
+
/** A prop as it is committed: a boolean view prop held as text is the boolean the text says. */
|
|
867
|
+
function committedProp(key, value) {
|
|
868
|
+
return typeof value === 'string' && BOOLEAN_VIEW_PROPS.has(key) ? value !== 'false' : value;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* The `pointer-events` an element ends up with, in the order its props are merged: its inline
|
|
872
|
+
* style's, then its `pointerEvents` prop, then what the sheets resolve. The inline one is read for
|
|
873
|
+
* itself because an element no rule matches resolves to what it inherits, with its own inline
|
|
874
|
+
* value only in what it hands down.
|
|
875
|
+
*/
|
|
876
|
+
function pointerEventsOf(node, resolved) {
|
|
877
|
+
const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
|
|
878
|
+
const own = inline?.['pointerEvents'];
|
|
879
|
+
// Merged over the prop, so its `inherit` is what the sheets and the parent settled, not the prop.
|
|
880
|
+
if (own === 'inherit')
|
|
881
|
+
return resolved['pointerEvents'];
|
|
882
|
+
return own ?? node.props['pointerEvents'] ?? resolved['pointerEvents'];
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* A node's `pointer-events` as native takes it, once its props and styles are merged.
|
|
886
|
+
*
|
|
887
|
+
* In CSS `none` is the element alone, and a descendant's `auto` takes touches again, which native
|
|
888
|
+
* calls `box-none`: what a `none` with something inside to open it is committed as. Native's own
|
|
889
|
+
* `none` is the whole subtree. It is what a `none` with nothing inside to open it stays, because
|
|
890
|
+
* Android's text and images take no `pointerEvents` of their own and only an ancestor's `none`
|
|
891
|
+
* keeps touches off them. It is also what the `pointerEvents` prop means, so a `none` that is the
|
|
892
|
+
* prop's is left as it is.
|
|
893
|
+
*
|
|
894
|
+
* An inline `inherit` is what the element has without it, which is `resolved`: what the sheets
|
|
895
|
+
* and its parent settled.
|
|
896
|
+
*/
|
|
897
|
+
function nativePointerEvents(node, style, resolved) {
|
|
898
|
+
let value = style['pointerEvents'];
|
|
899
|
+
if (value === undefined)
|
|
900
|
+
return;
|
|
901
|
+
if (value === 'inherit') {
|
|
902
|
+
value = resolved;
|
|
903
|
+
if (value === undefined)
|
|
904
|
+
delete style['pointerEvents'];
|
|
905
|
+
else
|
|
906
|
+
style['pointerEvents'] = value;
|
|
907
|
+
}
|
|
908
|
+
if (value === 'none' && node.touchWithin && node.props['pointerEvents'] !== 'none') {
|
|
909
|
+
style['pointerEvents'] = 'box-none';
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/** Whether `node`, whose `pointer-events` is `value`, takes touches or holds something that does. */
|
|
913
|
+
function takesTouches(node, value, prop) {
|
|
914
|
+
if (value === undefined)
|
|
915
|
+
return false;
|
|
916
|
+
// The prop's `none` is native's: the whole subtree, whatever is inside it.
|
|
917
|
+
if (value === 'none' && prop === 'none')
|
|
918
|
+
return false;
|
|
919
|
+
// CSS's `none` and native's `box-none` take no touches themselves: their children answer.
|
|
920
|
+
if (value === 'none' || value === 'box-none') {
|
|
921
|
+
return node.children.some((child) => child.touchWithin);
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Records on `node` whether it, or something inside it, takes touches where it would inherit
|
|
927
|
+
* `none`. When that changes a `none` element's own answer, its props are marked to be committed
|
|
928
|
+
* again.
|
|
929
|
+
*
|
|
930
|
+
* Called as a node is reconciled, after its children, so theirs are already current: a child that
|
|
931
|
+
* was skipped as clean has nothing beneath it that changed, and a change beneath marks every
|
|
932
|
+
* ancestor to come back through here.
|
|
933
|
+
*/
|
|
934
|
+
function noteTouches(node, style) {
|
|
935
|
+
if (!style)
|
|
936
|
+
return;
|
|
937
|
+
// Nearly every node: no `pointer-events` in any sheet, style or prop, and nothing recorded.
|
|
938
|
+
const resolved = style.style['pointerEvents'];
|
|
939
|
+
const prop = node.props['pointerEvents'];
|
|
940
|
+
if (resolved === undefined && prop === undefined && !node.inlineInherits && !node.touchWithin) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const value = pointerEventsOf(node, style.style);
|
|
944
|
+
const within = takesTouches(node, value, prop);
|
|
945
|
+
if (within === !!node.touchWithin)
|
|
946
|
+
return;
|
|
947
|
+
node.touchWithin = within;
|
|
948
|
+
if (value === 'none')
|
|
949
|
+
node.propsDirty = true;
|
|
517
950
|
}
|
|
518
951
|
/** Whether `node` is `ancestor` or somewhere under it. */
|
|
519
952
|
function isWithin(node, ancestor) {
|
|
@@ -616,6 +1049,10 @@ class RetainedNode {
|
|
|
616
1049
|
propsDirty = false;
|
|
617
1050
|
structureDirty = false;
|
|
618
1051
|
subtreeDirty = false;
|
|
1052
|
+
touchWithin = undefined;
|
|
1053
|
+
box = undefined;
|
|
1054
|
+
anonymous = undefined;
|
|
1055
|
+
styled = undefined;
|
|
619
1056
|
transitions;
|
|
620
1057
|
playing;
|
|
621
1058
|
scrolled;
|
|
@@ -749,8 +1186,9 @@ const IS_COLOR_PROP = /color(android)?$/i;
|
|
|
749
1186
|
const colorProps = new Map();
|
|
750
1187
|
function isColorProp(key) {
|
|
751
1188
|
let answer = colorProps.get(key);
|
|
752
|
-
if (answer === undefined)
|
|
753
|
-
colorProps.set(key, (answer = IS_COLOR_PROP.test(key)));
|
|
1189
|
+
if (answer === undefined) {
|
|
1190
|
+
colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || BRUSH_PROPS.has(key)));
|
|
1191
|
+
}
|
|
754
1192
|
return answer;
|
|
755
1193
|
}
|
|
756
1194
|
const ASSET_PROPS = new Set(['source', 'defaultSource', 'loadingIndicatorSource']);
|
|
@@ -763,7 +1201,30 @@ const ASSET_PROPS = new Set(['source', 'defaultSource', 'loadingIndicatorSource'
|
|
|
763
1201
|
* approximated, it is dropped.
|
|
764
1202
|
*/
|
|
765
1203
|
const NESTED_COLOR_LIST_PROPS = new Set(['boxShadow']);
|
|
1204
|
+
/** The props of an SVG shape that take a brush, not a colour. */
|
|
1205
|
+
const BRUSH_PROPS = new Set(['fill', 'stroke']);
|
|
766
1206
|
/** Styles arrive as objects, arrays, nested arrays and nulls. Reduce to one object. */
|
|
1207
|
+
/**
|
|
1208
|
+
* A bound transform is still the CSS string: see `inline-transform.ts`. One CSS cannot read
|
|
1209
|
+
* leaves the transform the rules set, as a browser drops it.
|
|
1210
|
+
*/
|
|
1211
|
+
function boundTransform(node, style, cascaded) {
|
|
1212
|
+
const text = style['transform'];
|
|
1213
|
+
if (typeof text !== 'string')
|
|
1214
|
+
return style;
|
|
1215
|
+
// The same list for the same string, commit after commit: a transition compares the two ends
|
|
1216
|
+
// it is given, and a list made afresh each time reads as a new place to go.
|
|
1217
|
+
if (node.boundTransform?.text !== text)
|
|
1218
|
+
node.boundTransform = { text, list: transformList(text) };
|
|
1219
|
+
const list = node.boundTransform.list ?? cascaded;
|
|
1220
|
+
if (list === undefined)
|
|
1221
|
+
delete style['transform'];
|
|
1222
|
+
else
|
|
1223
|
+
style['transform'] = list;
|
|
1224
|
+
return style;
|
|
1225
|
+
}
|
|
1226
|
+
/** A transform string as the list native reads, and none for one CSS cannot read. */
|
|
1227
|
+
const transformOf = (value) => transformList(value) ?? [];
|
|
767
1228
|
function flattenStyle(value, into) {
|
|
768
1229
|
if (!value)
|
|
769
1230
|
return into;
|
|
@@ -839,7 +1300,7 @@ function composeTransform(node, props) {
|
|
|
839
1300
|
}
|
|
840
1301
|
else {
|
|
841
1302
|
// A bound style's transform is still the CSS string: see `inline-transform.ts`.
|
|
842
|
-
const own = typeof transform === 'string' ?
|
|
1303
|
+
const own = typeof transform === 'string' ? transformOf(transform) : transform;
|
|
843
1304
|
value = [translate, rotate, scale, own].flatMap((part) => (Array.isArray(part) ? part : []));
|
|
844
1305
|
node.composedTransform = { parts, value };
|
|
845
1306
|
}
|
|
@@ -949,15 +1410,19 @@ export class Engine {
|
|
|
949
1410
|
* for them. Only those in the tree: see `releaseDetached`.
|
|
950
1411
|
*/
|
|
951
1412
|
hoisted = new Set();
|
|
952
|
-
/**
|
|
953
|
-
removedSinceCommit =
|
|
1413
|
+
/** The nodes taken out of a child list since the last commit. See `releaseDetached`. */
|
|
1414
|
+
removedSinceCommit = new Set();
|
|
954
1415
|
/**
|
|
955
|
-
* `@keyframes` by name, across every sheet
|
|
1416
|
+
* `@keyframes` by name, across every sheet registered. One registry rather than one per sheet,
|
|
956
1417
|
* because that is the scope CSS gives them: a name defined in a global stylesheet is usable
|
|
957
1418
|
* from a component's. The cost is that two components defining the same name collide, last one
|
|
958
1419
|
* in wins, exactly as two stylesheets in a document would.
|
|
959
1420
|
*/
|
|
960
1421
|
keyframes = new Map();
|
|
1422
|
+
/** Every sheet registered, in the order a document would hold them. See `sheetReplaced`. */
|
|
1423
|
+
sheetOrder = [];
|
|
1424
|
+
/** The `@keyframes` each playing animation was started from. See `startPlaying`. */
|
|
1425
|
+
playedFrames = new WeakMap();
|
|
961
1426
|
/** Element names already reported as unclaimed, so a list of a thousand rows reports once. */
|
|
962
1427
|
reported = new Set();
|
|
963
1428
|
processColor;
|
|
@@ -973,12 +1438,15 @@ export class Engine {
|
|
|
973
1438
|
const conditions = options.conditions ?? { width: 0, height: 0, colorScheme: 'light' };
|
|
974
1439
|
this.styles = new StyleResolver(options.globalStyles ?? null, conditions);
|
|
975
1440
|
this.viewportSize = { width: conditions.width, height: conditions.height };
|
|
1441
|
+
this.fontScale = conditions.fontScale;
|
|
976
1442
|
if (options.tokens)
|
|
977
1443
|
this.styles.setRootTokens(options.tokens);
|
|
978
1444
|
this.structuralSheets = options.globalStyles?.structural === true;
|
|
979
1445
|
this.dev = options.dev ?? globalThis.__DEV__ === true;
|
|
980
|
-
if (this.dev)
|
|
981
|
-
this.styles.onUndefinedToken = (name, props) => this.reportUndefinedToken(name, props);
|
|
1446
|
+
if (this.dev) {
|
|
1447
|
+
this.styles.onUndefinedToken = (name, props, on) => this.reportUndefinedToken(name, props, on);
|
|
1448
|
+
this.styles.onUnreadDisplay = (name, value) => this.reportUnreadDisplay(name, value);
|
|
1449
|
+
}
|
|
982
1450
|
this.now = options.now ?? (() => globalThis.performance?.now?.() ?? Date.now());
|
|
983
1451
|
this.onDirty = options.onDirty;
|
|
984
1452
|
this.onError = options.onError;
|
|
@@ -1050,6 +1518,35 @@ export class Engine {
|
|
|
1050
1518
|
* a caller owns the properties it names and nothing else. Each owner writes all of its own at
|
|
1051
1519
|
* once - four insets together, never one - so nothing is left over from a previous rotation.
|
|
1052
1520
|
*/
|
|
1521
|
+
/**
|
|
1522
|
+
* Match `sheet` against every node from now on, after the global sheet the engine was made
|
|
1523
|
+
* with, and restyle everything; false, and nothing done, when it already is. How a platform
|
|
1524
|
+
* applies a component whose CSS is not scoped, such as Angular's `ViewEncapsulation.None`.
|
|
1525
|
+
* `replacing` is a sheet added before that this one takes the place of, after a hot swap.
|
|
1526
|
+
*/
|
|
1527
|
+
addGlobalSheet(sheet, replacing) {
|
|
1528
|
+
if (!this.styles.addGlobalSheet(sheet, replacing))
|
|
1529
|
+
return false;
|
|
1530
|
+
if (replacing)
|
|
1531
|
+
this.sheetReplaced(replacing, sheet);
|
|
1532
|
+
else
|
|
1533
|
+
this.registerSheet(sheet);
|
|
1534
|
+
if (sheet.structural)
|
|
1535
|
+
this.structuralSheets = true;
|
|
1536
|
+
this.markPath(this.root);
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Stop matching a sheet `addGlobalSheet` added, and restyle everything; false, and nothing
|
|
1541
|
+
* done, when it is not one. A hot swap that leaves a None component no rules, or makes it scoped.
|
|
1542
|
+
*/
|
|
1543
|
+
removeGlobalSheet(sheet) {
|
|
1544
|
+
if (!this.styles.removeGlobalSheet(sheet))
|
|
1545
|
+
return false;
|
|
1546
|
+
this.sheetReplaced(sheet, null);
|
|
1547
|
+
this.markPath(this.root);
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1053
1550
|
updateTokens(next) {
|
|
1054
1551
|
this.styles.setRootTokens({ ...this.styles.rootTokens, ...next });
|
|
1055
1552
|
this.root.subtreeDirty = true;
|
|
@@ -1102,6 +1599,25 @@ export class Engine {
|
|
|
1102
1599
|
* text attributes change, which the same cap does. Android sets it only when `fontFamily` is in
|
|
1103
1600
|
* the props it is sent, and the engine sends only what changed, so it is sent again once.
|
|
1104
1601
|
*/
|
|
1602
|
+
/**
|
|
1603
|
+
* Loads have settled for these families: every node held back for one, a span inside a
|
|
1604
|
+
* paragraph included, is laid out with the name again, registered or not. See `loadingFamilies`.
|
|
1605
|
+
*/
|
|
1606
|
+
fontsSettled(families) {
|
|
1607
|
+
let held = false;
|
|
1608
|
+
const visit = (node) => {
|
|
1609
|
+
for (const child of node.children) {
|
|
1610
|
+
if (child.heldFamily !== undefined && families.has(child.heldFamily)) {
|
|
1611
|
+
this.markProps(child, false);
|
|
1612
|
+
held = true;
|
|
1613
|
+
}
|
|
1614
|
+
visit(child);
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
visit(this.root);
|
|
1618
|
+
if (held)
|
|
1619
|
+
this.commit();
|
|
1620
|
+
}
|
|
1105
1621
|
fontsRegistered(families) {
|
|
1106
1622
|
const visit = (node) => {
|
|
1107
1623
|
for (const child of node.children) {
|
|
@@ -1189,23 +1705,57 @@ export class Engine {
|
|
|
1189
1705
|
this.markProps(node, false);
|
|
1190
1706
|
this.commit();
|
|
1191
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* A node's component rules are its creating sheet's, so this matches it against `like`'s, or
|
|
1710
|
+
* against the one it was created with once `like` is null.
|
|
1711
|
+
*/
|
|
1712
|
+
adoptScope(node, like) {
|
|
1713
|
+
if (like && !this.ownSheets.has(node))
|
|
1714
|
+
this.ownSheets.set(node, node.sheet);
|
|
1715
|
+
const sheet = like ? like.sheet : this.ownSheets.get(node);
|
|
1716
|
+
if (sheet === undefined || node.sheet === sheet)
|
|
1717
|
+
return;
|
|
1718
|
+
node.sheet = sheet;
|
|
1719
|
+
if (sheet?.structural)
|
|
1720
|
+
this.structuralSheets = true;
|
|
1721
|
+
this.markProps(node);
|
|
1722
|
+
}
|
|
1723
|
+
/** The sheet each node given another's scope was created with. */
|
|
1724
|
+
ownSheets = new WeakMap();
|
|
1192
1725
|
/** The window's size, from the conditions media queries use. Zero until the platform says. */
|
|
1193
1726
|
get viewport() {
|
|
1194
1727
|
return this.viewportSize;
|
|
1195
1728
|
}
|
|
1196
1729
|
viewportSize;
|
|
1730
|
+
/** The system text size, from the conditions. See `Conditions.fontScale`. */
|
|
1731
|
+
fontScale;
|
|
1732
|
+
viewportWatchers = new Set();
|
|
1733
|
+
/** Calls `listener` after each change of conditions, which is when `viewport` can change. */
|
|
1734
|
+
watchViewport(listener) {
|
|
1735
|
+
this.viewportWatchers.add(listener);
|
|
1736
|
+
return () => this.viewportWatchers.delete(listener);
|
|
1737
|
+
}
|
|
1197
1738
|
updateConditions(next) {
|
|
1198
1739
|
this.viewportSize = { width: next.width, height: next.height };
|
|
1740
|
+
this.fontScale = next.fontScale;
|
|
1199
1741
|
this.styles.setConditions(next);
|
|
1200
1742
|
this.root.subtreeDirty = true;
|
|
1201
1743
|
// Committed here rather than left to the next change-detection pass, because there may not
|
|
1202
1744
|
// be one: no view is dirty, so a tick does no work and never reaches the renderer at all.
|
|
1203
1745
|
this.commit();
|
|
1746
|
+
for (const watcher of this.viewportWatchers)
|
|
1747
|
+
watcher();
|
|
1204
1748
|
}
|
|
1205
1749
|
// --- mutation API -----------------------------------------------------
|
|
1206
1750
|
createElement(name, sheet = null) {
|
|
1207
1751
|
const node = new RetainedNode('element', name, this);
|
|
1208
1752
|
node.sheet = sheet;
|
|
1753
|
+
const html = HTML_ELEMENTS.get(name);
|
|
1754
|
+
if (html !== undefined) {
|
|
1755
|
+
if (html.role)
|
|
1756
|
+
node.props['accessibilityRole'] = html.role;
|
|
1757
|
+
node.styled = html.styled;
|
|
1758
|
+
}
|
|
1209
1759
|
if (sheet?.structural)
|
|
1210
1760
|
this.structuralSheets = true;
|
|
1211
1761
|
if (HOISTS[name])
|
|
@@ -1220,6 +1770,26 @@ export class Engine {
|
|
|
1220
1770
|
node.classes?.delete(name);
|
|
1221
1771
|
this.markProps(node);
|
|
1222
1772
|
}
|
|
1773
|
+
/**
|
|
1774
|
+
* The sheet of the component `node` hosts, whose `:host` rules it matches. Set again when a hot
|
|
1775
|
+
* swap replaces the component's sheet, which recreates the views inside but not the host, so the
|
|
1776
|
+
* host is styled afresh here.
|
|
1777
|
+
*/
|
|
1778
|
+
setHostSheet(node, sheet) {
|
|
1779
|
+
if (node.hostSheet === sheet)
|
|
1780
|
+
return;
|
|
1781
|
+
node.hostSheet = sheet;
|
|
1782
|
+
this.markProps(node);
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Style for `node` that any matched rule or inline style overrides, as a browser's own default
|
|
1786
|
+
* for an element is. `mount` gives the root component's host the surface's height this way, as
|
|
1787
|
+
* the web's mount point has, unless the app's `:host` sizes it otherwise.
|
|
1788
|
+
*/
|
|
1789
|
+
setDefaultStyle(node, style) {
|
|
1790
|
+
node.defaultStyle = style;
|
|
1791
|
+
this.markProps(node);
|
|
1792
|
+
}
|
|
1223
1793
|
/** `class="a b"` from a template. Replaces the set rather than adding to it. */
|
|
1224
1794
|
setClasses(node, value) {
|
|
1225
1795
|
const names = value.split(/\s+/).filter(Boolean);
|
|
@@ -1241,7 +1811,8 @@ export class Engine {
|
|
|
1241
1811
|
parent.children.push(child);
|
|
1242
1812
|
if (child.dormantHoists)
|
|
1243
1813
|
this.wakeHoists(child);
|
|
1244
|
-
this.markStructure(parent);
|
|
1814
|
+
this.markStructure(parent, child);
|
|
1815
|
+
this.markTextContent(parent, child);
|
|
1245
1816
|
}
|
|
1246
1817
|
insertBefore(parent, child, ref) {
|
|
1247
1818
|
this.detach(child);
|
|
@@ -1253,7 +1824,8 @@ export class Engine {
|
|
|
1253
1824
|
parent.children.splice(at, 0, child);
|
|
1254
1825
|
if (child.dormantHoists)
|
|
1255
1826
|
this.wakeHoists(child);
|
|
1256
|
-
this.markStructure(parent);
|
|
1827
|
+
this.markStructure(parent, child);
|
|
1828
|
+
this.markTextContent(parent, child);
|
|
1257
1829
|
}
|
|
1258
1830
|
removeChild(parent, child) {
|
|
1259
1831
|
const target = parent ?? child.parent;
|
|
@@ -1264,8 +1836,17 @@ export class Engine {
|
|
|
1264
1836
|
return;
|
|
1265
1837
|
target.children.splice(at, 1);
|
|
1266
1838
|
child.parent = null;
|
|
1267
|
-
this.removedSinceCommit
|
|
1268
|
-
this.markStructure(target);
|
|
1839
|
+
this.removedSinceCommit.add(child);
|
|
1840
|
+
this.markStructure(target, child);
|
|
1841
|
+
this.markTextContent(target, child);
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* A child came or went under a view that takes its text as a prop: that prop changed. A span
|
|
1845
|
+
* changes it too, by keeping or dropping the space before it.
|
|
1846
|
+
*/
|
|
1847
|
+
markTextContent(parent, child) {
|
|
1848
|
+
if (child.kind !== 'anchor' && takesTextAsProp(parent))
|
|
1849
|
+
this.markProps(parent);
|
|
1269
1850
|
}
|
|
1270
1851
|
/** A subtree that went out of the tree is back in: its hoisted nodes commit again. */
|
|
1271
1852
|
wakeHoists(node) {
|
|
@@ -1288,12 +1869,17 @@ export class Engine {
|
|
|
1288
1869
|
* whole. So a hoisted node moves to the root of the subtree it left with, which is garbage along
|
|
1289
1870
|
* with it if the subtree never returns, and is handed back when it does. An animation on a node
|
|
1290
1871
|
* out of the tree stops rather than running a frame loop for something nobody can see, and
|
|
1291
|
-
* starts again if the node returns.
|
|
1872
|
+
* starts again if the node returns. And a node out of the tree when the commit runs forgets its
|
|
1873
|
+
* committed views, as a withheld modal does: the commit unmounts them, Fabric never re-enables an
|
|
1874
|
+
* unmounted view's events (`EventEmitter::setEnabled`), and committed again they would ignore
|
|
1875
|
+
* every touch. It comes back created afresh, as React creates an element it mounts again. One
|
|
1876
|
+
* put back before the commit never left, and keeps its views.
|
|
1292
1877
|
*
|
|
1293
|
-
* Once per commit, and only after a removal: the walks are the depth of a handful of nodes
|
|
1878
|
+
* Once per commit, and only after a removal: the walks are the depth of a handful of nodes, and
|
|
1879
|
+
* forgetting walks each subtree that left once.
|
|
1294
1880
|
*/
|
|
1295
1881
|
releaseDetached() {
|
|
1296
|
-
this.
|
|
1882
|
+
this.forgetDetached();
|
|
1297
1883
|
for (const node of this.hoisted) {
|
|
1298
1884
|
const top = this.topOf(node);
|
|
1299
1885
|
if (top === this.root)
|
|
@@ -1319,6 +1905,14 @@ export class Engine {
|
|
|
1319
1905
|
if (this.focusedNode && this.topOf(this.focusedNode) !== this.root)
|
|
1320
1906
|
this.focusedNode = null;
|
|
1321
1907
|
}
|
|
1908
|
+
/** Forget the committed views of each node removed since the last commit that is still out. */
|
|
1909
|
+
forgetDetached() {
|
|
1910
|
+
for (const node of this.removedSinceCommit) {
|
|
1911
|
+
if (node.committed && this.topOf(node) !== this.root)
|
|
1912
|
+
this.forgetCommitted(node);
|
|
1913
|
+
}
|
|
1914
|
+
this.removedSinceCommit.clear();
|
|
1915
|
+
}
|
|
1322
1916
|
/** The root of the tree a node is in: the engine's root while it is attached. */
|
|
1323
1917
|
topOf(node) {
|
|
1324
1918
|
let top = node;
|
|
@@ -1373,7 +1967,17 @@ export class Engine {
|
|
|
1373
1967
|
// Inline style is applied after the cascade and inherited by nothing, so it cannot change
|
|
1374
1968
|
// what any node matches or inherits. Every other prop can: `[disabled]` is a selector. An
|
|
1375
1969
|
// inline `direction` is the exception, because the paragraphs below it align by it.
|
|
1376
|
-
this.markProps(node, key !== 'style' || this.
|
|
1970
|
+
this.markProps(node, key !== 'style' || this.inlineReachesStyle(node));
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Whether a node's inline style can change what it or anything below it resolves to: it sets,
|
|
1974
|
+
* or until now set, a property its children inherit.
|
|
1975
|
+
*/
|
|
1976
|
+
inlineReachesStyle(node) {
|
|
1977
|
+
const inherits = setsInherited(node.props['style']);
|
|
1978
|
+
const inherited = node.inlineInherits === true;
|
|
1979
|
+
node.inlineInherits = inherits;
|
|
1980
|
+
return this.noteInlineDirection(node) || inherits || inherited;
|
|
1377
1981
|
}
|
|
1378
1982
|
/** Whether a node's inline style sets `direction`, remembering that one has. */
|
|
1379
1983
|
noteInlineDirection(node) {
|
|
@@ -1395,7 +1999,26 @@ export class Engine {
|
|
|
1395
1999
|
* has changed for the props diff to notice.
|
|
1396
2000
|
*/
|
|
1397
2001
|
styleChanged(node) {
|
|
1398
|
-
this.markProps(node, this.
|
|
2002
|
+
this.markProps(node, this.inlineReachesStyle(node));
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* A bound declaration, `[style.color]`, whose value may be a `var()`. One that is, is kept as a
|
|
2006
|
+
* declaration the cascade settles with the node's tokens, as a rule's is, and true is answered:
|
|
2007
|
+
* the text is no value to put in the node's style. Any other value forgets what was kept.
|
|
2008
|
+
*/
|
|
2009
|
+
setBoundStyle(node, prop, value) {
|
|
2010
|
+
const text = typeof value === 'string' && /^\s*var\(/i.test(value) ? value : undefined;
|
|
2011
|
+
const bound = text === undefined ? null : boundDeclaration(prop, tokenFromValue(text), node.name);
|
|
2012
|
+
if (!bound && !node.boundByProp?.has(prop))
|
|
2013
|
+
return false;
|
|
2014
|
+
const byProp = (node.boundByProp ??= new Map());
|
|
2015
|
+
if (bound)
|
|
2016
|
+
byProp.set(prop, bound);
|
|
2017
|
+
else
|
|
2018
|
+
byProp.delete(prop);
|
|
2019
|
+
node.boundStyle = byProp.size ? [...byProp.values()] : null;
|
|
2020
|
+
this.markProps(node);
|
|
2021
|
+
return bound !== null;
|
|
1399
2022
|
}
|
|
1400
2023
|
/**
|
|
1401
2024
|
* Set a custom property on a node, `--tint` from `[style.--tint]`, or remove it with `null`.
|
|
@@ -1421,6 +2044,8 @@ export class Engine {
|
|
|
1421
2044
|
setText(node, value) {
|
|
1422
2045
|
node.text = value;
|
|
1423
2046
|
this.markProps(node);
|
|
2047
|
+
if (node.parent)
|
|
2048
|
+
this.markTextContent(node.parent, node);
|
|
1424
2049
|
}
|
|
1425
2050
|
setEventListener(node, topLevelType, fn) {
|
|
1426
2051
|
this.registerEventHandler();
|
|
@@ -1460,7 +2085,8 @@ export class Engine {
|
|
|
1460
2085
|
siblings[i].styleDirty = true;
|
|
1461
2086
|
}
|
|
1462
2087
|
}
|
|
1463
|
-
|
|
2088
|
+
/** `node`'s child list changed: `moved` came into it or went out of it. */
|
|
2089
|
+
markStructure(node, moved) {
|
|
1464
2090
|
node.structureDirty = true;
|
|
1465
2091
|
// A child list that moved changes what its members match, though nothing about them did:
|
|
1466
2092
|
// the old last row is no longer the last. Only sheets that ask about position pay for this.
|
|
@@ -1468,6 +2094,10 @@ export class Engine {
|
|
|
1468
2094
|
node.styleDirty = true;
|
|
1469
2095
|
for (const child of node.children)
|
|
1470
2096
|
child.styleDirty = true;
|
|
2097
|
+
// `.box:empty + .spacer`: what a later sibling matches can hang on whether this node is
|
|
2098
|
+
// empty, which only its first child arriving or its last one leaving changes.
|
|
2099
|
+
if (wasOrIsEmptyWithout(node, moved))
|
|
2100
|
+
this.markLaterSiblings(node);
|
|
1471
2101
|
}
|
|
1472
2102
|
this.markPath(node.parent);
|
|
1473
2103
|
}
|
|
@@ -1494,7 +2124,7 @@ export class Engine {
|
|
|
1494
2124
|
* optimisation.
|
|
1495
2125
|
*/
|
|
1496
2126
|
commit() {
|
|
1497
|
-
if (this.removedSinceCommit)
|
|
2127
|
+
if (this.removedSinceCommit.size)
|
|
1498
2128
|
this.releaseDetached();
|
|
1499
2129
|
// The root is never reconciled itself, so it has no `committed` record and `isClean` would
|
|
1500
2130
|
// always say dirty. Ask the flags directly, or every change-detection pass ends in a
|
|
@@ -1511,6 +2141,8 @@ export class Engine {
|
|
|
1511
2141
|
this.facesAdded = false;
|
|
1512
2142
|
const set = this.fabric.createChildSet(this.rootTag);
|
|
1513
2143
|
for (const child of this.visibleChildren(this.root)) {
|
|
2144
|
+
if (this.withheld(child))
|
|
2145
|
+
continue;
|
|
1514
2146
|
this.fabric.appendChildToSet(set, this.reconcileUnder(this.root, child));
|
|
1515
2147
|
}
|
|
1516
2148
|
this.clearFlags(this.root);
|
|
@@ -1529,10 +2161,59 @@ export class Engine {
|
|
|
1529
2161
|
}
|
|
1530
2162
|
this.flushTransitionEvents();
|
|
1531
2163
|
this.scrollDriver?.afterCommit();
|
|
1532
|
-
|
|
2164
|
+
// Read first: the commit that settles keyframes starts by clearing it.
|
|
2165
|
+
const facesAdded = this.facesAdded;
|
|
2166
|
+
if (this.awaitingKeyframes.size)
|
|
2167
|
+
this.settleKeyframes();
|
|
2168
|
+
if (facesAdded)
|
|
1533
2169
|
this.rematchFonts();
|
|
1534
2170
|
return true;
|
|
1535
2171
|
}
|
|
2172
|
+
/** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
|
|
2173
|
+
unseen = Infinity;
|
|
2174
|
+
/**
|
|
2175
|
+
* A commit for what changed in the same turn as earlier commits, before a frame was drawn.
|
|
2176
|
+
* `since` is how many commits there had been before the first of them (`stats.commits`).
|
|
2177
|
+
*
|
|
2178
|
+
* A view those commits created has not been seen in the style they gave it, so what this one
|
|
2179
|
+
* gives it is where it starts, and no transition runs toward it. That is what a browser
|
|
2180
|
+
* does with a class added to an element in the turn that inserted it, and what `animate.enter`
|
|
2181
|
+
* is built on: Angular adds the enter class after the render pass that created the element, and
|
|
2182
|
+
* takes it off a frame later, which is the one change the transition is for.
|
|
2183
|
+
*/
|
|
2184
|
+
commitUnseen(since) {
|
|
2185
|
+
this.unseen = since;
|
|
2186
|
+
try {
|
|
2187
|
+
return this.commit();
|
|
2188
|
+
}
|
|
2189
|
+
finally {
|
|
2190
|
+
this.unseen = Infinity;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
/** Nodes that asked for `@keyframes` this commit had not met, by the name they asked for. */
|
|
2194
|
+
awaitingKeyframes = new Map();
|
|
2195
|
+
/**
|
|
2196
|
+
* A sheet is registered when the first node it styles is committed, so a node committed before
|
|
2197
|
+
* it can name keyframes that arrive later in the same commit. Those nodes play them from another
|
|
2198
|
+
* commit straight away, as a browser applies keyframes however the sheets arrived; a name still
|
|
2199
|
+
* unknown once the commit is over is reported.
|
|
2200
|
+
*/
|
|
2201
|
+
settleKeyframes() {
|
|
2202
|
+
const waiting = [...this.awaitingKeyframes];
|
|
2203
|
+
this.awaitingKeyframes.clear();
|
|
2204
|
+
let found = false;
|
|
2205
|
+
for (const [node, name] of waiting) {
|
|
2206
|
+
if (this.keyframes.has(name)) {
|
|
2207
|
+
this.markProps(node, false);
|
|
2208
|
+
found = true;
|
|
2209
|
+
}
|
|
2210
|
+
else if (this.dev) {
|
|
2211
|
+
this.reportMissingKeyframes(name);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
if (found)
|
|
2215
|
+
this.commit();
|
|
2216
|
+
}
|
|
1536
2217
|
scrollDriver;
|
|
1537
2218
|
/** Whether native can move a view by a scroll with no JavaScript in between. */
|
|
1538
2219
|
get drivesScroll() {
|
|
@@ -1599,12 +2280,14 @@ export class Engine {
|
|
|
1599
2280
|
}
|
|
1600
2281
|
/** Anchors take part in sibling ordering but never reach Fabric. */
|
|
1601
2282
|
visibleChildren(node) {
|
|
1602
|
-
|
|
2283
|
+
// A view that takes its text as a prop has no text children to commit. See `ViewNameOptions`.
|
|
2284
|
+
const textAsProp = takesTextAsProp(node);
|
|
2285
|
+
return node.children.filter((child) => child.kind !== 'anchor' && !(textAsProp && child.kind === 'text'));
|
|
1603
2286
|
}
|
|
1604
2287
|
/**
|
|
1605
|
-
* Everything a node renders with, unprocessed. Precedence, weakest first: native defaults,
|
|
1606
|
-
* matched CSS, explicit props, inline style, a component's
|
|
1607
|
-
* on the web.
|
|
2288
|
+
* Everything a node renders with, unprocessed. Precedence, weakest first: native defaults, the
|
|
2289
|
+
* node's `defaultStyle`, matched CSS, explicit props, inline style, a component's
|
|
2290
|
+
* `styleOverride`. Inline wins over CSS for the same reason it does on the web.
|
|
1608
2291
|
*
|
|
1609
2292
|
* Raw, because this is what the next commit diffs against. Colours and asset ids are converted
|
|
1610
2293
|
* on the way out instead (`processed`), and only for the keys that changed: the converters
|
|
@@ -1618,29 +2301,28 @@ export class Engine {
|
|
|
1618
2301
|
this.checkProps(node);
|
|
1619
2302
|
this.registerSheet(node.sheet);
|
|
1620
2303
|
this.registerSheet(node.hostSheet);
|
|
1621
|
-
const props = { ...DEFAULT_PROPS[viewName] };
|
|
2304
|
+
const props = { ...DEFAULT_PROPS[viewName], ...node.defaultStyle };
|
|
1622
2305
|
Object.assign(props, this.styles.resolve(node, this.styleEpoch).style);
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
!key.includes('-')) {
|
|
1630
|
-
props[key] = node.props[key];
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
const style = flattenStyle(node.props['style'], props);
|
|
2306
|
+
const resolved = props['pointerEvents'];
|
|
2307
|
+
writeOwnProps(node.props, props);
|
|
2308
|
+
withTextContent(node, viewName, props);
|
|
2309
|
+
const cascaded = props['transform'];
|
|
2310
|
+
const style = boundTransform(node, flattenStyle(node.props['style'], props), cascaded);
|
|
2311
|
+
nativePointerEvents(node, style, resolved);
|
|
1634
2312
|
const intrinsic = node.props[INTRINSIC_SIZE];
|
|
1635
2313
|
if (intrinsic)
|
|
1636
2314
|
applyIntrinsicSize(style, intrinsic);
|
|
1637
2315
|
flattenStyle(node.props[STYLE_OVERRIDE], style);
|
|
1638
2316
|
this.fontFaces.apply(style);
|
|
2317
|
+
holdLoadingFamily(node, style);
|
|
1639
2318
|
if (viewName === PARAGRAPH)
|
|
1640
2319
|
alignText(style, this.directionOf(node, style));
|
|
1641
2320
|
if (this.fontsRefreshed)
|
|
1642
2321
|
this.capForFonts(node, style);
|
|
1643
|
-
|
|
2322
|
+
alignMultiline(viewName, style);
|
|
2323
|
+
const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
2324
|
+
centreSingleLine(viewName, merged, this.fontScale);
|
|
2325
|
+
return merged;
|
|
1644
2326
|
}
|
|
1645
2327
|
/**
|
|
1646
2328
|
* The text size cap that lays a paragraph or text input out again once a face it names has
|
|
@@ -1698,12 +2380,70 @@ export class Engine {
|
|
|
1698
2380
|
if (!sheet || this.knownSheets.has(sheet))
|
|
1699
2381
|
return;
|
|
1700
2382
|
this.knownSheets.add(sheet);
|
|
2383
|
+
this.sheetOrder.push(sheet);
|
|
1701
2384
|
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
1702
2385
|
this.keyframes.set(name, sheet.keyframes[name]);
|
|
1703
2386
|
}
|
|
1704
2387
|
if (sheet.fonts && this.fontFaces.add(sheet.fonts))
|
|
1705
2388
|
this.facesAdded = true;
|
|
1706
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* A hot swap edited a component's sheet into `next`, or took it away with null: `sheet`'s
|
|
2392
|
+
* `@keyframes` go, and `next`'s take its place among the others, as an edited stylesheet keeps
|
|
2393
|
+
* its place in a document. A name another sheet also defines falls back to that one. Every
|
|
2394
|
+
* animation re-resolves the name it plays, so one whose keyframes went stops, as in a browser.
|
|
2395
|
+
* A `sheet` already gone, such as a global sheet removed, leaves `next` registered last.
|
|
2396
|
+
*/
|
|
2397
|
+
sheetReplaced(sheet, next) {
|
|
2398
|
+
if (sheet === next && this.knownSheets.has(sheet))
|
|
2399
|
+
return;
|
|
2400
|
+
const at = this.sheetOrder.indexOf(sheet);
|
|
2401
|
+
if (at === -1) {
|
|
2402
|
+
this.registerSheet(next);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
this.knownSheets.delete(sheet);
|
|
2406
|
+
if (next && !this.knownSheets.has(next)) {
|
|
2407
|
+
this.knownSheets.add(next);
|
|
2408
|
+
this.sheetOrder[at] = next;
|
|
2409
|
+
}
|
|
2410
|
+
else {
|
|
2411
|
+
this.sheetOrder.splice(at, 1);
|
|
2412
|
+
}
|
|
2413
|
+
if (sheet.keyframes || next?.keyframes)
|
|
2414
|
+
this.reindexKeyframes();
|
|
2415
|
+
if (sheet.fonts || next?.fonts)
|
|
2416
|
+
this.reindexFonts();
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Every sheet's `@font-face` rules again, in order, and every text that names a family matched
|
|
2420
|
+
* again: a face whose rule went is no longer matched, though the platform keeps it registered.
|
|
2421
|
+
*/
|
|
2422
|
+
reindexFonts() {
|
|
2423
|
+
this.fontFaces.clear();
|
|
2424
|
+
for (const sheet of this.sheetOrder)
|
|
2425
|
+
if (sheet.fonts)
|
|
2426
|
+
this.fontFaces.add(sheet.fonts);
|
|
2427
|
+
this.markFontText(this.root);
|
|
2428
|
+
}
|
|
2429
|
+
/** Every sheet's `@keyframes` again, in order, and every animation told to look them up. */
|
|
2430
|
+
reindexKeyframes() {
|
|
2431
|
+
this.keyframes.clear();
|
|
2432
|
+
for (const sheet of this.sheetOrder) {
|
|
2433
|
+
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
2434
|
+
this.keyframes.set(name, sheet.keyframes[name]);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
this.markAnimated(this.root);
|
|
2438
|
+
}
|
|
2439
|
+
/** Mark every node under `node` with an animation, so it looks its keyframes up again. */
|
|
2440
|
+
markAnimated(node) {
|
|
2441
|
+
for (const child of node.children) {
|
|
2442
|
+
if (child.playing || child.scrolled)
|
|
2443
|
+
this.markProps(child, false);
|
|
2444
|
+
this.markAnimated(child);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
1707
2447
|
/** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
|
|
1708
2448
|
fontFaces = new FontFaces();
|
|
1709
2449
|
/** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
|
|
@@ -1714,18 +2454,19 @@ export class Engine {
|
|
|
1714
2454
|
* Once per sheet that declares faces, so walking the tree costs nothing that matters.
|
|
1715
2455
|
*/
|
|
1716
2456
|
rematchFonts() {
|
|
1717
|
-
|
|
1718
|
-
for (const child of node.children) {
|
|
1719
|
-
if (child.kind !== 'element')
|
|
1720
|
-
continue;
|
|
1721
|
-
if (typeof child.committed?.props['fontFamily'] === 'string')
|
|
1722
|
-
this.markProps(child, false);
|
|
1723
|
-
visit(child);
|
|
1724
|
-
}
|
|
1725
|
-
};
|
|
1726
|
-
visit(this.root);
|
|
2457
|
+
this.markFontText(this.root);
|
|
1727
2458
|
this.commit();
|
|
1728
2459
|
}
|
|
2460
|
+
/** Mark every element under `node` committed with a family, so it is matched again. */
|
|
2461
|
+
markFontText(node) {
|
|
2462
|
+
for (const child of node.children) {
|
|
2463
|
+
if (child.kind !== 'element')
|
|
2464
|
+
continue;
|
|
2465
|
+
if (typeof child.committed?.props['fontFamily'] === 'string')
|
|
2466
|
+
this.markProps(child, false);
|
|
2467
|
+
this.markFontText(child);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
1729
2470
|
knownSheets = new WeakSet();
|
|
1730
2471
|
/**
|
|
1731
2472
|
* Start, advance or stop the `@keyframes` animation a node's style asks for.
|
|
@@ -1740,6 +2481,7 @@ export class Engine {
|
|
|
1740
2481
|
this.stopScrolled(node);
|
|
1741
2482
|
if (!spec) {
|
|
1742
2483
|
if (node.playing) {
|
|
2484
|
+
this.cancelPlaying(node);
|
|
1743
2485
|
node.playing = undefined;
|
|
1744
2486
|
this.playing.delete(node);
|
|
1745
2487
|
}
|
|
@@ -1747,8 +2489,13 @@ export class Engine {
|
|
|
1747
2489
|
}
|
|
1748
2490
|
const frames = this.keyframes.get(spec.name);
|
|
1749
2491
|
if (!frames) {
|
|
1750
|
-
|
|
1751
|
-
|
|
2492
|
+
// Keyframes a hot swap deleted, from under an animation that was playing them, or ones a
|
|
2493
|
+
// sheet later in this commit has: see `settleKeyframes`.
|
|
2494
|
+
this.cancelPlaying(node);
|
|
2495
|
+
node.playing = undefined;
|
|
2496
|
+
this.playing.delete(node);
|
|
2497
|
+
this.stopScrolled(node);
|
|
2498
|
+
this.awaitingKeyframes.set(node, spec.name);
|
|
1752
2499
|
return props;
|
|
1753
2500
|
}
|
|
1754
2501
|
if (spec.timeline)
|
|
@@ -1756,13 +2503,38 @@ export class Engine {
|
|
|
1756
2503
|
this.startPlaying(node, spec, frames, props);
|
|
1757
2504
|
return Object.assign(props, node.playing?.values ?? {});
|
|
1758
2505
|
}
|
|
2506
|
+
/**
|
|
2507
|
+
* A clock animation the scroll plays instead: the same animation where only the timeline moved,
|
|
2508
|
+
* and one cancelled where the name changed.
|
|
2509
|
+
*/
|
|
2510
|
+
handOverToScroll(node, spec) {
|
|
2511
|
+
if (!node.playing)
|
|
2512
|
+
return;
|
|
2513
|
+
if (node.playing.spec.name !== spec.name)
|
|
2514
|
+
this.cancelPlaying(node);
|
|
2515
|
+
node.playing = undefined;
|
|
2516
|
+
this.playing.delete(node);
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* `animationcancel` for an animation stopped before it ended: the element no longer asks for it,
|
|
2520
|
+
* asks for another by name, or its keyframes went, as a browser fires it.
|
|
2521
|
+
*/
|
|
2522
|
+
cancelPlaying(node) {
|
|
2523
|
+
const running = node.playing;
|
|
2524
|
+
if (running && !running.done)
|
|
2525
|
+
this.emitTransition(node, 'topAnimationcancel', running.spec.name);
|
|
2526
|
+
}
|
|
1759
2527
|
/** Start the clock on an animation, unless the node is already playing this one. */
|
|
1760
2528
|
startPlaying(node, spec, frames, props) {
|
|
1761
2529
|
const current = node.playing;
|
|
2530
|
+
const inherited = this.inheritedColour(node, frames);
|
|
1762
2531
|
if (!current || !sameAnimation(current.spec, spec)) {
|
|
2532
|
+
if (current && current.spec.name !== spec.name)
|
|
2533
|
+
this.cancelPlaying(node);
|
|
1763
2534
|
const started = {
|
|
1764
2535
|
spec,
|
|
1765
|
-
tracks: tracksOf(frames, props),
|
|
2536
|
+
tracks: tracksOf(frames, props, inherited),
|
|
2537
|
+
inherited,
|
|
1766
2538
|
start: this.now(),
|
|
1767
2539
|
values: {},
|
|
1768
2540
|
done: false,
|
|
@@ -1775,6 +2547,7 @@ export class Engine {
|
|
|
1775
2547
|
// resting style: `animation-fill-mode: backwards` behaviour, always. It is what an entrance
|
|
1776
2548
|
// wants, and the alternative is the flash again.
|
|
1777
2549
|
started.values = sample(started, this.now()).values;
|
|
2550
|
+
this.playedFrames.set(started, frames);
|
|
1778
2551
|
node.playing = started;
|
|
1779
2552
|
if (spec.paused)
|
|
1780
2553
|
started.pausedAt = this.now();
|
|
@@ -1783,7 +2556,34 @@ export class Engine {
|
|
|
1783
2556
|
this.emitTransition(node, 'topAnimationstart', spec.name);
|
|
1784
2557
|
return;
|
|
1785
2558
|
}
|
|
1786
|
-
this.
|
|
2559
|
+
if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
|
|
2560
|
+
this.reframe(node, current, frames, props, inherited);
|
|
2561
|
+
}
|
|
2562
|
+
this.playOrPause(node, node.playing, spec);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* The colour `node` inherits, for frames that set `color: currentColor`, which is that colour
|
|
2566
|
+
* wherever the animation has got to, as in CSS. Undefined for frames that do not.
|
|
2567
|
+
*/
|
|
2568
|
+
inheritedColour(node, frames) {
|
|
2569
|
+
if (!readsInheritedColour(frames))
|
|
2570
|
+
return undefined;
|
|
2571
|
+
const parent = node.parent && this.styles.resolve(node.parent, this.styleEpoch);
|
|
2572
|
+
return parent?.inherited['color'] ?? 'black';
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* The name an animation plays now has other frames: a hot swap edited them, or took away the
|
|
2576
|
+
* sheet whose copy won. Or the colour its `color: currentColor` stands for has changed. It
|
|
2577
|
+
* carries on along the new tracks, on its own clock, as a browser does, paused or not.
|
|
2578
|
+
*/
|
|
2579
|
+
reframe(node, current, frames, props, inherited) {
|
|
2580
|
+
const tracks = tracksOf(frames, props, inherited);
|
|
2581
|
+
const reframed = { ...current, tracks, inherited };
|
|
2582
|
+
const { values, finished } = sample(reframed, current.pausedAt ?? this.now());
|
|
2583
|
+
const holds = current.spec.fill === 'forwards' || current.spec.fill === 'both';
|
|
2584
|
+
reframed.values = finished && !holds ? {} : values;
|
|
2585
|
+
this.playedFrames.set(reframed, frames);
|
|
2586
|
+
node.playing = reframed;
|
|
1787
2587
|
}
|
|
1788
2588
|
/**
|
|
1789
2589
|
* `animation-play-state` changed on an animation already playing: pausing holds the frame it
|
|
@@ -1817,15 +2617,13 @@ export class Engine {
|
|
|
1817
2617
|
*/
|
|
1818
2618
|
scrollAnimated(node, spec, frames, props) {
|
|
1819
2619
|
const current = node.scrolled;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
this.stopScrolled(node);
|
|
1824
|
-
if (node.playing) {
|
|
1825
|
-
node.playing = undefined;
|
|
1826
|
-
this.playing.delete(node);
|
|
2620
|
+
const inherited = this.inheritedColour(node, frames);
|
|
2621
|
+
if (current?.frames === frames && sameAnimation(current.spec, spec)) {
|
|
2622
|
+
return Object.assign(props, this.rescrolled(node, current, frames, props, inherited));
|
|
1827
2623
|
}
|
|
1828
|
-
|
|
2624
|
+
this.stopScrolled(node);
|
|
2625
|
+
this.handOverToScroll(node, spec);
|
|
2626
|
+
const tracks = tracksOf(frames, props, inherited);
|
|
1829
2627
|
const source = this.scrollSourceOf(node);
|
|
1830
2628
|
const extent = source ? (this.scrollExtents.get(source)?.[spec.timeline] ?? null) : null;
|
|
1831
2629
|
const range = rangeOf(spec, extent);
|
|
@@ -1838,9 +2636,31 @@ export class Engine {
|
|
|
1838
2636
|
// view for the animation to move. Committed with the first frame, so it stays put.
|
|
1839
2637
|
if (drive)
|
|
1840
2638
|
first['collapsable'] = false;
|
|
1841
|
-
node.scrolled = { spec, tracks, resting, first, source, drive };
|
|
2639
|
+
node.scrolled = { spec, frames, tracks, resting, first, source, drive, inherited };
|
|
1842
2640
|
return Object.assign(props, first);
|
|
1843
2641
|
}
|
|
2642
|
+
/**
|
|
2643
|
+
* The first frame of a scroll-driven animation already playing. When the colour its
|
|
2644
|
+
* `color: currentColor` stands for has changed, that frame's colour is taken from tracks built
|
|
2645
|
+
* again. Native drives opacity and transforms only, so a colour holds its first frame, and
|
|
2646
|
+
* nothing native animates is touched.
|
|
2647
|
+
*/
|
|
2648
|
+
rescrolled(node, current, frames, props, inherited) {
|
|
2649
|
+
if (inherited === current.inherited)
|
|
2650
|
+
return current.first;
|
|
2651
|
+
// The element's own colour, which a track with no first frame starts at, may be the one it
|
|
2652
|
+
// inherits, so it is read again too.
|
|
2653
|
+
const resting = { ...current.resting, color: props['color'] };
|
|
2654
|
+
const tracks = tracksOf(frames, resting, inherited);
|
|
2655
|
+
const extent = current.source ? this.scrollExtents.get(current.source) : undefined;
|
|
2656
|
+
const range = rangeOf(current.spec, extent?.[current.spec.timeline] ?? null);
|
|
2657
|
+
const first = { ...current.first };
|
|
2658
|
+
const colour = firstFrame(tracks, current.spec, range)['color'];
|
|
2659
|
+
if ('color' in first)
|
|
2660
|
+
first['color'] = colour;
|
|
2661
|
+
node.scrolled = { ...current, tracks, resting, first, inherited };
|
|
2662
|
+
return first;
|
|
2663
|
+
}
|
|
1844
2664
|
/** Hand a scroll-driven animation's channels to native, fed by `source`'s scroll events. */
|
|
1845
2665
|
driveScrolled(node, source, spec, tracks, resting, range) {
|
|
1846
2666
|
if (!this.scrollDriver)
|
|
@@ -1919,6 +2739,9 @@ export class Engine {
|
|
|
1919
2739
|
`has loaded, so the block has to exist somewhere by the time the animation starts.`);
|
|
1920
2740
|
}
|
|
1921
2741
|
transitioned(node, props) {
|
|
2742
|
+
// Nobody saw what the last commit gave a view it created, so this is where the view starts.
|
|
2743
|
+
if (node.bornIn >= this.unseen)
|
|
2744
|
+
node.transitions = undefined;
|
|
1922
2745
|
const spec = transitionSpec(props);
|
|
1923
2746
|
if (!spec && !node.transitions)
|
|
1924
2747
|
return props;
|
|
@@ -2030,7 +2853,7 @@ export class Engine {
|
|
|
2030
2853
|
if (value === null || value === undefined)
|
|
2031
2854
|
out[key] = value;
|
|
2032
2855
|
else if (isColorProp(key))
|
|
2033
|
-
out[key] = this.
|
|
2856
|
+
out[key] = this.painted(key, value);
|
|
2034
2857
|
// A list of sources has already been resolved by whoever built it, as RN's resolver
|
|
2035
2858
|
// also assumes: it passes any object through and only turns a number into one.
|
|
2036
2859
|
else if (ASSET_PROPS.has(key) && !Array.isArray(value))
|
|
@@ -2043,7 +2866,7 @@ export class Engine {
|
|
|
2043
2866
|
out[key] = this.processFilters(value);
|
|
2044
2867
|
// A bound style's transform is still the CSS string: see `inline-transform.ts`.
|
|
2045
2868
|
else if (key === 'transform' && typeof value === 'string')
|
|
2046
|
-
out[key] =
|
|
2869
|
+
out[key] = transformOf(value);
|
|
2047
2870
|
else
|
|
2048
2871
|
out[key] = value;
|
|
2049
2872
|
}
|
|
@@ -2114,9 +2937,11 @@ export class Engine {
|
|
|
2114
2937
|
if (this.isClean(node) && !styleChanged)
|
|
2115
2938
|
return node.committed.handle;
|
|
2116
2939
|
node.styleCommitted = style;
|
|
2117
|
-
const childHandles = this.reconcileChildren(node, style);
|
|
2118
|
-
const previous = node.committed;
|
|
2119
2940
|
const viewName = viewNameOf(node);
|
|
2941
|
+
this.forgetRenamed(node, viewName);
|
|
2942
|
+
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
2943
|
+
noteTouches(node, style);
|
|
2944
|
+
const previous = node.committed;
|
|
2120
2945
|
this.notePresented(node, viewName);
|
|
2121
2946
|
if (!previous)
|
|
2122
2947
|
return this.create(node, viewName, this.mergeProps(node, viewName), childHandles);
|
|
@@ -2134,7 +2959,7 @@ export class Engine {
|
|
|
2134
2959
|
handle = this.clone(previous.handle, propsPayload && this.processed(propsPayload), childrenChanged ? childHandles : null);
|
|
2135
2960
|
// A clone is the same view, so it keeps the tag it was created with, not the newest tag
|
|
2136
2961
|
// handed out, which is some other node's.
|
|
2137
|
-
node.committed = { handle, tag: previous.tag, props, childHandles };
|
|
2962
|
+
node.committed = { handle, tag: previous.tag, props, childHandles, viewName };
|
|
2138
2963
|
}
|
|
2139
2964
|
this.clearFlags(node);
|
|
2140
2965
|
return handle;
|
|
@@ -2168,7 +2993,7 @@ export class Engine {
|
|
|
2168
2993
|
* handle as it stands, so it is answered here without the call, the resolve and the walk -
|
|
2169
2994
|
* which, per child, was most of what a one-row update cost.
|
|
2170
2995
|
*/
|
|
2171
|
-
reconcileChildren(node, style) {
|
|
2996
|
+
reconcileChildren(node, viewName, style) {
|
|
2172
2997
|
const context = style?.context;
|
|
2173
2998
|
if (HOIST_TARGETS.has(node.name))
|
|
2174
2999
|
return this.reconcileHoistTarget(node, context);
|
|
@@ -2177,20 +3002,57 @@ export class Engine {
|
|
|
2177
3002
|
if (child.kind === 'anchor' || this.committedElsewhere(child) || this.withheld(child)) {
|
|
2178
3003
|
continue;
|
|
2179
3004
|
}
|
|
3005
|
+
if (child.kind === 'text') {
|
|
3006
|
+
const handle = this.reconcileText(node, child, viewName, context);
|
|
3007
|
+
if (handle)
|
|
3008
|
+
handles.push(handle);
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
2180
3011
|
handles.push(this.reconcileUnder(node, child, context));
|
|
2181
3012
|
}
|
|
2182
3013
|
return handles;
|
|
2183
3014
|
}
|
|
2184
3015
|
/**
|
|
2185
|
-
*
|
|
3016
|
+
* The paragraph a run of text written straight into a view is drawn in, or nothing for a run of
|
|
3017
|
+
* whitespace, which a browser draws nothing for between the items of a flex box either.
|
|
2186
3018
|
*
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
*
|
|
2192
|
-
*
|
|
3019
|
+
* It is the text's own, kept on the text node, and no part of the tree: its parent is the view,
|
|
3020
|
+
* so it inherits what the view's text would, and no rule matches it.
|
|
3021
|
+
*/
|
|
3022
|
+
/**
|
|
3023
|
+
* A run of text under `parent`, which commits as `viewName`: the text itself inside a text, a
|
|
3024
|
+
* paragraph of its own inside any other view, and nothing where the view takes its text as a
|
|
3025
|
+
* prop (see `ViewNameOptions`) or the run is whitespace between a view's children.
|
|
2193
3026
|
*/
|
|
3027
|
+
reconcileText(parent, text, viewName, context) {
|
|
3028
|
+
if (TEXT_CONTENT_PROPS[viewName] !== undefined)
|
|
3029
|
+
return null;
|
|
3030
|
+
if (viewName === PARAGRAPH || viewName === VIRTUAL_TEXT) {
|
|
3031
|
+
// Under a text element, the paragraph it had in a view is done with. Under that paragraph
|
|
3032
|
+
// itself, which is where a loose run is committed, it is the one to keep.
|
|
3033
|
+
if (text.box && text.box !== parent)
|
|
3034
|
+
text.box = undefined;
|
|
3035
|
+
return this.reconcileUnder(parent, text, context);
|
|
3036
|
+
}
|
|
3037
|
+
const box = this.looseText(parent, text);
|
|
3038
|
+
return box ? this.reconcileUnder(parent, box, context) : null;
|
|
3039
|
+
}
|
|
3040
|
+
looseText(parent, text) {
|
|
3041
|
+
if (!trimEnds(text.text))
|
|
3042
|
+
return null;
|
|
3043
|
+
let box = text.box;
|
|
3044
|
+
if (!box) {
|
|
3045
|
+
box = new RetainedNode('element', 'text', this);
|
|
3046
|
+
box.anonymous = true;
|
|
3047
|
+
box.children = [text];
|
|
3048
|
+
text.box = box;
|
|
3049
|
+
}
|
|
3050
|
+
box.parent = parent;
|
|
3051
|
+
// The text is not under the box in the tree, so a change to it never marked the box.
|
|
3052
|
+
if (!this.isClean(text))
|
|
3053
|
+
box.subtreeDirty = true;
|
|
3054
|
+
return box;
|
|
3055
|
+
}
|
|
2194
3056
|
/** Remember an iOS modal host committed as showing, so hiding it waits for `topDismiss`. */
|
|
2195
3057
|
notePresented(node, viewName) {
|
|
2196
3058
|
if (viewName !== MODAL_HOST || node.props['visible'] === false)
|
|
@@ -2198,10 +3060,28 @@ export class Engine {
|
|
|
2198
3060
|
if (platformOS === 'ios')
|
|
2199
3061
|
node.presented = true;
|
|
2200
3062
|
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Whether a child is left out of its parent's commit: a modal host whose `visible` is false.
|
|
3065
|
+
*
|
|
3066
|
+
* The native host covers the screen and takes every touch whether or not it presents anything,
|
|
3067
|
+
* which is why React Native's Modal.js renders null while hidden. This is the same thing, so a
|
|
3068
|
+
* hidden `<modal>` leaves the screen under it usable. On iOS a modal that was showing stays
|
|
3069
|
+
* committed, with `visible` false, until native reports `topDismiss`: Modal.js keeps it that
|
|
3070
|
+
* long so the dismissal animates and `(dismiss)` fires. Android drops it at once, as Modal.js
|
|
3071
|
+
* does there.
|
|
3072
|
+
*
|
|
3073
|
+
* A withheld host is unmounted, and Fabric drops an unmounted view's event target for good
|
|
3074
|
+
* (`EventEmitter::setEnabled`), so its committed handles are forgotten and it is created afresh
|
|
3075
|
+
* when it is shown again, as Modal.js creates it afresh. Committed again, it ignores every touch.
|
|
3076
|
+
*/
|
|
2201
3077
|
withheld(child) {
|
|
2202
3078
|
if (child.props['visible'] !== false || child.presented)
|
|
2203
3079
|
return false;
|
|
2204
|
-
|
|
3080
|
+
if (viewNameOf(child) !== MODAL_HOST)
|
|
3081
|
+
return false;
|
|
3082
|
+
if (child.committed)
|
|
3083
|
+
this.forgetCommitted(child);
|
|
3084
|
+
return true;
|
|
2205
3085
|
}
|
|
2206
3086
|
/**
|
|
2207
3087
|
* `reconcileChildren` for a node other nodes hoist into: its own children, each followed by
|
|
@@ -2252,14 +3132,29 @@ export class Engine {
|
|
|
2252
3132
|
child.committedUnder = parent;
|
|
2253
3133
|
return handle;
|
|
2254
3134
|
}
|
|
2255
|
-
/**
|
|
3135
|
+
/**
|
|
3136
|
+
* A node whose native view is no longer the one it was created as is created again: a text
|
|
3137
|
+
* element that came to hold a view, or stopped. A clone keeps the view it was made as.
|
|
3138
|
+
*/
|
|
3139
|
+
forgetRenamed(node, viewName) {
|
|
3140
|
+
if (node.committed && node.committed.viewName !== viewName)
|
|
3141
|
+
this.forgetCommitted(node);
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Let a node and everything under it be created again at the next commit. The views it keeps
|
|
3145
|
+
* for hoisted names go too: they are children of the native view it is losing.
|
|
3146
|
+
*/
|
|
2256
3147
|
forgetCommitted(node) {
|
|
2257
3148
|
node.committed = null;
|
|
2258
3149
|
node.committedUnder = null;
|
|
2259
3150
|
node.styleCommitted = null;
|
|
2260
|
-
|
|
2261
|
-
|
|
3151
|
+
node.kept = undefined;
|
|
3152
|
+
if (node.box?.committed)
|
|
3153
|
+
this.forgetCommitted(node.box);
|
|
3154
|
+
for (const child of node.children) {
|
|
3155
|
+
if (child.committed || child.box?.committed)
|
|
2262
3156
|
this.forgetCommitted(child);
|
|
3157
|
+
}
|
|
2263
3158
|
}
|
|
2264
3159
|
/** A hoisted node, which commits into an ancestor further up than the parent it is in. */
|
|
2265
3160
|
committedElsewhere(child) {
|
|
@@ -2301,7 +3196,7 @@ export class Engine {
|
|
|
2301
3196
|
};
|
|
2302
3197
|
const payload = diffProps(kept.committed.props, props);
|
|
2303
3198
|
const handle = this.clone(kept.committed.handle, payload && this.processed(payload), []);
|
|
2304
|
-
kept.committed = {
|
|
3199
|
+
kept.committed = { ...kept.committed, handle, props, childHandles: [] };
|
|
2305
3200
|
kept.node = null;
|
|
2306
3201
|
}
|
|
2307
3202
|
handles.push(kept.committed.handle);
|
|
@@ -2370,13 +3265,14 @@ export class Engine {
|
|
|
2370
3265
|
const handle = this.fabric.createNode(tag, viewName, this.rootTag, this.processed(props), node);
|
|
2371
3266
|
for (const child of childHandles)
|
|
2372
3267
|
this.fabric.appendChild(handle, child);
|
|
2373
|
-
node.committed = { handle, tag, props, childHandles };
|
|
3268
|
+
node.committed = { handle, tag, props, childHandles, viewName };
|
|
3269
|
+
node.bornIn = this.stats.commits;
|
|
2374
3270
|
this.clearFlags(node);
|
|
2375
3271
|
return handle;
|
|
2376
3272
|
}
|
|
2377
3273
|
/**
|
|
2378
3274
|
* Angular's own unknown-element check does not run in this pipeline, so a `<text>` in a
|
|
2379
|
-
* template that never imported `Text` compiles,
|
|
3275
|
+
* template that never imported `Text` compiles, draws without the component, and looks like a
|
|
2380
3276
|
* layout bug. On the first commit of any known element with no component behind it, say so.
|
|
2381
3277
|
*/
|
|
2382
3278
|
checkClaimed(node) {
|
|
@@ -2387,7 +3283,8 @@ export class Engine {
|
|
|
2387
3283
|
this.reported.add(node.name);
|
|
2388
3284
|
console.error(`[angular-native] <${node.name}> is used in a template that does not import ` +
|
|
2389
3285
|
`${className(node.name)}. Add it to the component's \`imports\` from ` +
|
|
2390
|
-
`'@ng-native/components'; without it the element
|
|
3286
|
+
`'@ng-native/components'; without it the element has none of the component's inputs ` +
|
|
3287
|
+
`or behavior, so a prop written on it stays the text it was written as.`);
|
|
2391
3288
|
}
|
|
2392
3289
|
/**
|
|
2393
3290
|
* The misspelt-prop check. Angular's own "Can't bind to" check needs a DOM to run, so a
|
|
@@ -2398,14 +3295,30 @@ export class Engine {
|
|
|
2398
3295
|
*/
|
|
2399
3296
|
undefinedTokens = new Set();
|
|
2400
3297
|
/** Said once per name: a phone has no inspector to show the declaration struck out. */
|
|
2401
|
-
reportUndefinedToken(name, props) {
|
|
3298
|
+
reportUndefinedToken(name, props, on) {
|
|
2402
3299
|
if (this.undefinedTokens.has(name))
|
|
2403
3300
|
return;
|
|
2404
3301
|
this.undefinedTokens.add(name);
|
|
2405
|
-
|
|
3302
|
+
// A bound declaration has no rule to be found by, so it says which element it is on.
|
|
3303
|
+
const where = on === undefined ? props.join(', ') : `${props.join(', ')}, bound on <${on}>,`;
|
|
3304
|
+
console.warn(`[angular-native] var(${name}) in ${where} names a custom property nothing in` +
|
|
2406
3305
|
` scope defines, so the declaration is dropped, as a browser drops it. Define ${name} on` +
|
|
2407
3306
|
` :root or an ancestor, or give the var() a fallback.`);
|
|
2408
3307
|
}
|
|
3308
|
+
unreadDisplays = new Set();
|
|
3309
|
+
/**
|
|
3310
|
+
* Said once per token and value: the compiler refuses `display: grid` written out, but a token
|
|
3311
|
+
* is only known here, and without this the view lays out as flex with nothing said.
|
|
3312
|
+
*/
|
|
3313
|
+
reportUnreadDisplay(name, value) {
|
|
3314
|
+
const report = `${name} ${value}`;
|
|
3315
|
+
if (this.unreadDisplays.has(report))
|
|
3316
|
+
return;
|
|
3317
|
+
this.unreadDisplays.add(report);
|
|
3318
|
+
console.warn(`[angular-native] display: var(${name}) is ${value ?? 'no keyword'}, which is no display` +
|
|
3319
|
+
` native has, so display is unset and the view lays out as flex. Native has flex, none` +
|
|
3320
|
+
` and contents, and reads block, inline, inline-block, flow-root and inline-flex as flex.`);
|
|
3321
|
+
}
|
|
2409
3322
|
checkProps(node) {
|
|
2410
3323
|
if (!node.claimed || !PRIMITIVE_NAMES.has(node.name))
|
|
2411
3324
|
return;
|
|
@@ -2617,6 +3530,18 @@ export class Engine {
|
|
|
2617
3530
|
resolveAsset(value) {
|
|
2618
3531
|
return this.resolveAssetSource(value);
|
|
2619
3532
|
}
|
|
3533
|
+
/**
|
|
3534
|
+
* A colour prop as native takes it. A shape's `fill` or `stroke` the cascade settled from a
|
|
3535
|
+
* token is the solid brush react-native-svg takes, and `none` no paint at all.
|
|
3536
|
+
*/
|
|
3537
|
+
painted(key, value) {
|
|
3538
|
+
if (!BRUSH_PROPS.has(key))
|
|
3539
|
+
return this.color(value);
|
|
3540
|
+
// Already a brush, as the icon wrote it: only one the cascade settled is still text.
|
|
3541
|
+
if (typeof value !== 'string')
|
|
3542
|
+
return value;
|
|
3543
|
+
return value === 'none' ? null : { type: 0, payload: this.color(value) };
|
|
3544
|
+
}
|
|
2620
3545
|
/**
|
|
2621
3546
|
* The host's `processColor`, and the one place a platform colour becomes one.
|
|
2622
3547
|
*
|
|
@@ -2767,7 +3692,7 @@ export class Engine {
|
|
|
2767
3692
|
// Listeners get RN's documented shape, `{nativeEvent}`. Fabric hands us the payload bare;
|
|
2768
3693
|
// React wraps it in a synthetic event and every RN API is written against `event.nativeEvent`,
|
|
2769
3694
|
// so handlers ported from RN would silently read undefined otherwise.
|
|
2770
|
-
const event = new SyntheticEvent(nativeEvent);
|
|
3695
|
+
const event = new SyntheticEvent(nativeEvent, target);
|
|
2771
3696
|
try {
|
|
2772
3697
|
if (topLevelType === 'topDismiss' && target)
|
|
2773
3698
|
this.dismissed(target);
|