@ng-native/fabric 0.5.0 → 0.7.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 +212 -31
- package/dist/css.js +814 -134
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +187 -2
- package/dist/engine.js +930 -88
- package/dist/engine.js.map +1 -1
- package/dist/inline-token.js +25 -12
- package/dist/inline-token.js.map +1 -1
- package/dist/inline-transform.js +21 -3
- package/dist/inline-transform.js.map +1 -1
- package/dist/native-drive.js +5 -0
- package/dist/native-drive.js.map +1 -1
- package/dist/scroll-animation.d.ts +13 -0
- package/dist/scroll-animation.js +53 -4
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +25 -1
- package/dist/transition.js +179 -30
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* because a persistent parent holds references to specific child handles. That bubble is
|
|
8
8
|
* inherent and is what React's own Fabric renderer does.
|
|
9
9
|
*/
|
|
10
|
-
import { boundDeclaration, inlineInherited, setsInherited, StyleResolver, usesActive, } from "./css.js";
|
|
10
|
+
import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, placeReadElsewhere, siblingReach, } from "./css.js";
|
|
11
11
|
import { applyAria } from "./aria-props.js";
|
|
12
12
|
import { STYLED_ELEMENTS } from "./element-styles.js";
|
|
13
|
-
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
|
|
13
|
+
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, 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, scrollFeed, } from "./native-drive.js";
|
|
18
|
-
import { clockChannels, firstFrame, rangeOf, scrollChannels } from "./scroll-animation.js";
|
|
18
|
+
import { clockChannels, firstFrame, movesByShare, rangeOf, scrollChannels, sharesAsPoints, } from "./scroll-animation.js";
|
|
19
19
|
import { FontFaces } from "./font-faces.js";
|
|
20
20
|
import { premultipliedStops } from "./premultiplied-stops.js";
|
|
21
21
|
/**
|
|
@@ -389,6 +389,33 @@ const VIRTUAL_TEXT = 'VirtualText';
|
|
|
389
389
|
const DEFAULT_VIEW = 'View';
|
|
390
390
|
/** A top-level text, the one view that aligns its lines. A nested text is a `VirtualText`. */
|
|
391
391
|
const PARAGRAPH = 'Paragraph';
|
|
392
|
+
/**
|
|
393
|
+
* The box Yoga takes a view's baseline from where the view has none of its own: its first child
|
|
394
|
+
* that is laid out in its flow. Words written straight into the view are the paragraph the
|
|
395
|
+
* engine makes around them, and a child that is not displayed has no view to ask.
|
|
396
|
+
*/
|
|
397
|
+
function firstInFlow(node) {
|
|
398
|
+
for (const child of node.children ?? []) {
|
|
399
|
+
// Not the paragraph itself, which holds the words it was made around.
|
|
400
|
+
if (child.kind === 'text' && child.box)
|
|
401
|
+
return child.box === node ? undefined : child.box;
|
|
402
|
+
if (child.kind !== 'element' || ownLayout(child, 'display') === 'none')
|
|
403
|
+
continue;
|
|
404
|
+
if (ownLayout(child, 'position') !== 'absolute')
|
|
405
|
+
return child;
|
|
406
|
+
}
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
const isNode = (node) => node !== undefined;
|
|
410
|
+
/** Whether a row lays a child out among its others, and so can take its baseline from it. */
|
|
411
|
+
const inFlow = (child) => child.kind === 'element' && ownLayout(child, 'position') !== 'absolute';
|
|
412
|
+
/** The text field a row's box takes its baseline from: itself, or the first in its flow down. */
|
|
413
|
+
function fieldOnBaseline(item) {
|
|
414
|
+
let at = item;
|
|
415
|
+
while (at?.kind === 'element' && !TEXT_INPUTS.has(viewNameOf(at)))
|
|
416
|
+
at = firstInFlow(at);
|
|
417
|
+
return at?.kind === 'element' ? at : undefined;
|
|
418
|
+
}
|
|
392
419
|
const textDirection = (value) => value === 'ltr' || value === 'rtl' ? value : undefined;
|
|
393
420
|
/**
|
|
394
421
|
* What iOS scales a text's `lineHeight` by: the system text size, capped by the text's own
|
|
@@ -401,6 +428,89 @@ function textScale(props, fontScale = 1) {
|
|
|
401
428
|
const cap = props['maxFontSizeMultiplier'];
|
|
402
429
|
return typeof cap === 'number' && cap >= 1 ? Math.min(fontScale, cap) : fontScale;
|
|
403
430
|
}
|
|
431
|
+
/** A field's padding and border on its top or bottom edge, as they are written. */
|
|
432
|
+
const blockEdge = (props, side, logical) => [
|
|
433
|
+
props[`padding${side}`] ??
|
|
434
|
+
props[`paddingBlock${logical}`] ??
|
|
435
|
+
props['paddingBlock'] ??
|
|
436
|
+
props['paddingVertical'] ??
|
|
437
|
+
props['padding'] ??
|
|
438
|
+
0,
|
|
439
|
+
props[`border${side}Width`] ?? props['borderWidth'] ?? 0,
|
|
440
|
+
];
|
|
441
|
+
/**
|
|
442
|
+
* A multiline field's `rows` as its height, where it was given none: that many lines of its
|
|
443
|
+
* line height at the system text size, inside its padding and border, as a browser sizes a `<textarea>`. A native field
|
|
444
|
+
* is as tall as its text, which is one line before anything is typed.
|
|
445
|
+
*/
|
|
446
|
+
function rowsTall(props, fontScale) {
|
|
447
|
+
const rows = Number(props['rows']);
|
|
448
|
+
const line = props['lineHeight'];
|
|
449
|
+
if (props['multiline'] !== true || !(rows > 0) || typeof line !== 'number')
|
|
450
|
+
return;
|
|
451
|
+
if (isSet(props['height']) && props['height'] !== 'auto')
|
|
452
|
+
return;
|
|
453
|
+
// A content box's height is its lines alone: Yoga adds the padding and border itself.
|
|
454
|
+
const edges = props['boxSizing'] === 'content-box'
|
|
455
|
+
? []
|
|
456
|
+
: [...blockEdge(props, 'Top', 'Start'), ...blockEdge(props, 'Bottom', 'End')];
|
|
457
|
+
if (!edges.every((part) => typeof part === 'number'))
|
|
458
|
+
return;
|
|
459
|
+
// At the system text size, which the field's lines are drawn at.
|
|
460
|
+
const lines = rows * line * textScale(props, fontScale);
|
|
461
|
+
props['height'] = edges.reduce((sum, part) => sum + part, lines);
|
|
462
|
+
}
|
|
463
|
+
/** A field's own minimum height: none where it says `auto`, which a web element starts with. */
|
|
464
|
+
const ownMinimum = (minHeight) => (minHeight === 'auto' ? 0 : (minHeight ?? 0));
|
|
465
|
+
/** How tall the system's font is, as a share of its size: its rise over the baseline and fall under. */
|
|
466
|
+
const FONT_LINE = 0.95 + 0.24;
|
|
467
|
+
/**
|
|
468
|
+
* How tall a field is by its line: the line, padding and border of a border-box field, Yoga's
|
|
469
|
+
* default, and its content alone for a content-box one, which Yoga adds the padding and border
|
|
470
|
+
* to itself. In a row aligned by baseline some of the line is kept as padding: `lineBaseline`.
|
|
471
|
+
*/
|
|
472
|
+
function lineBox(props, box, onBaseline, fontScale) {
|
|
473
|
+
const half = onBaseline ? lineBaseline(props, box[0], box, fontScale) : 0;
|
|
474
|
+
if (props['boxSizing'] === 'content-box')
|
|
475
|
+
return box[0] - 2 * half;
|
|
476
|
+
return box.reduce((sum, part) => sum + part, 0);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* `lineBaseline` for a field a height sizes: the room is what the height leaves its text, and
|
|
480
|
+
* the field stays that height. A content-box field's height is its content's, so what is now
|
|
481
|
+
* padding comes out of it.
|
|
482
|
+
*/
|
|
483
|
+
function heightBaseline(props, height, onBaseline, fontScale) {
|
|
484
|
+
if (!onBaseline)
|
|
485
|
+
return;
|
|
486
|
+
const edges = [...blockEdge(props, 'Top', 'Start'), ...blockEdge(props, 'Bottom', 'End')];
|
|
487
|
+
if (!edges.every((part) => typeof part === 'number'))
|
|
488
|
+
return;
|
|
489
|
+
const content = props['boxSizing'] === 'content-box';
|
|
490
|
+
const room = content ? height : edges.reduce((left, part) => left - part, height);
|
|
491
|
+
const half = lineBaseline(props, room, [room, ...edges], fontScale);
|
|
492
|
+
if (content)
|
|
493
|
+
props['height'] = height - 2 * half;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Give a field with its line height left out the baseline of a paragraph in a line as tall, for
|
|
497
|
+
* a field in a row aligned by baseline: half the room the line has over the font is kept as
|
|
498
|
+
* padding over and under the text. React Native says a field's baseline from its text and its
|
|
499
|
+
* top padding, so without this it is the font's own, and the row lifts the paragraphs beside the
|
|
500
|
+
* field to it: a form field's prefix and suffix sit higher than what is typed. Answers the half.
|
|
501
|
+
*
|
|
502
|
+
* ponytail: the font is taken to be as tall as the system's, `FONT_LINE` of its size. Read from
|
|
503
|
+
* the font if a field in another face is seen a point off its prefix.
|
|
504
|
+
*/
|
|
505
|
+
function lineBaseline(props, line, [, top, , bottom], fontScale) {
|
|
506
|
+
const size = props['fontSize'];
|
|
507
|
+
if (typeof size !== 'number')
|
|
508
|
+
return 0;
|
|
509
|
+
const half = Math.max(0, (line - size * textScale(props, fontScale) * FONT_LINE) / 2);
|
|
510
|
+
props['paddingTop'] = top + half;
|
|
511
|
+
props['paddingBottom'] = bottom + half;
|
|
512
|
+
return half;
|
|
513
|
+
}
|
|
404
514
|
/**
|
|
405
515
|
* Centre the text of a single-line text field that has a line height, as Chrome centres an
|
|
406
516
|
* input's, keeping the height the line height gives it.
|
|
@@ -421,7 +531,7 @@ function textScale(props, fontScale = 1) {
|
|
|
421
531
|
* height alone for a content-box one. A null `height` is no height. A value that is not a number
|
|
422
532
|
* cannot be added up, and leaves the field as it was.
|
|
423
533
|
*/
|
|
424
|
-
function centreSingleLine(viewName, props, fontScale) {
|
|
534
|
+
function centreSingleLine(viewName, props, fontScale, onBaseline = false) {
|
|
425
535
|
const lineHeight = props['lineHeight'];
|
|
426
536
|
if (!TEXT_INPUTS.has(viewName) || typeof lineHeight !== 'number')
|
|
427
537
|
return;
|
|
@@ -430,6 +540,7 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
430
540
|
const height = props['height'];
|
|
431
541
|
if (typeof height === 'number') {
|
|
432
542
|
delete props['lineHeight'];
|
|
543
|
+
heightBaseline(props, height, onBaseline, fontScale);
|
|
433
544
|
return;
|
|
434
545
|
}
|
|
435
546
|
// A percentage is a fixed height only where the parent's is definite, known at layout alone.
|
|
@@ -438,35 +549,40 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
438
549
|
// Android sizes the field by its line height, and centres the text in it, itself.
|
|
439
550
|
if (viewName !== 'TextInput')
|
|
440
551
|
return;
|
|
441
|
-
const edge = (side, logical) =>
|
|
442
|
-
props[`padding${side}`] ??
|
|
443
|
-
props[`paddingBlock${logical}`] ??
|
|
444
|
-
props['paddingBlock'] ??
|
|
445
|
-
props['paddingVertical'] ??
|
|
446
|
-
props['padding'] ??
|
|
447
|
-
0,
|
|
448
|
-
props[`border${side}Width`] ?? props['borderWidth'] ?? 0,
|
|
449
|
-
];
|
|
552
|
+
const edge = (side, logical) => blockEdge(props, side, logical);
|
|
450
553
|
const line = lineHeight * textScale(props, fontScale);
|
|
451
554
|
const box = [line, ...edge('Top', 'Start'), ...edge('Bottom', 'End')];
|
|
452
|
-
const own = props['minHeight']
|
|
555
|
+
const own = ownMinimum(props['minHeight']);
|
|
453
556
|
const max = props['maxHeight'] ?? Infinity;
|
|
454
557
|
if (![...box, own, max].every((part) => typeof part === 'number'))
|
|
455
558
|
return;
|
|
456
559
|
delete props['lineHeight'];
|
|
457
|
-
|
|
458
|
-
const content = props['boxSizing'] === 'content-box'
|
|
459
|
-
? line
|
|
460
|
-
: box.reduce((sum, part) => sum + part, 0);
|
|
560
|
+
const content = lineBox(props, box, onBaseline, fontScale);
|
|
461
561
|
props['minHeight'] = Math.max(own, Math.min(content, max));
|
|
462
562
|
}
|
|
563
|
+
const NO_STYLE = {};
|
|
564
|
+
/**
|
|
565
|
+
* An element's inline style, less each property a rule it matches declared `!important`: an
|
|
566
|
+
* inline style is the last of the plain declarations, under every important one.
|
|
567
|
+
*
|
|
568
|
+
* ponytail: by the name a property is committed under, so an inline `padding` is not taken out
|
|
569
|
+
* by an important `padding-top`. Expand the inline shorthand here if one is ever written so.
|
|
570
|
+
*/
|
|
571
|
+
function inlineOf(node) {
|
|
572
|
+
if (!node.props['style'])
|
|
573
|
+
return NO_STYLE;
|
|
574
|
+
const inline = flattenStyle(node.props['style'], {});
|
|
575
|
+
for (const key of node.styleCache?.important ?? [])
|
|
576
|
+
delete inline[key];
|
|
577
|
+
return inline;
|
|
578
|
+
}
|
|
463
579
|
/**
|
|
464
580
|
* What a container's own style says for `key`, in the order `mergeProps` applies them: a
|
|
465
581
|
* component's override, inline, a prop, its stylesheet, its default.
|
|
466
582
|
*/
|
|
467
583
|
function ownLayout(node, key) {
|
|
468
584
|
return (flattenStyle(node.props[STYLE_OVERRIDE], {})[key] ??
|
|
469
|
-
|
|
585
|
+
inlineOf(node)[key] ??
|
|
470
586
|
node.props[key] ??
|
|
471
587
|
node.styleCache?.style[key] ??
|
|
472
588
|
node.defaultStyle?.[key]);
|
|
@@ -504,6 +620,10 @@ const STRETCHES = new Set([undefined, null, 'auto', 'stretch']);
|
|
|
504
620
|
* kept, since it stops the stretch already.
|
|
505
621
|
*/
|
|
506
622
|
function fitContent(node, props) {
|
|
623
|
+
const capped = {
|
|
624
|
+
row: capAtContent(props, 'width', 'maxWidth'),
|
|
625
|
+
column: capAtContent(props, 'height', 'maxHeight'),
|
|
626
|
+
};
|
|
507
627
|
const width = props['width'] === 'fit-content';
|
|
508
628
|
const height = props['height'] === 'fit-content';
|
|
509
629
|
if (!width && !height)
|
|
@@ -518,12 +638,83 @@ function fitContent(node, props) {
|
|
|
518
638
|
const container = containerOf(parent);
|
|
519
639
|
parent.fitContainer = container;
|
|
520
640
|
const [direction, align] = container.split(' ');
|
|
641
|
+
if (capped[direction === 'row' ? 'row' : 'column'])
|
|
642
|
+
withoutGrowth(props);
|
|
521
643
|
const across = direction === 'row' ? height : width;
|
|
522
644
|
if (across && align === 'stretch' && STRETCHES.has(props['alignSelf'])) {
|
|
523
645
|
props['alignSelf'] = 'flex-start';
|
|
524
646
|
}
|
|
525
647
|
}
|
|
648
|
+
/**
|
|
649
|
+
* A box capped at its content along its container's main axis, where it would grow: a browser
|
|
650
|
+
* clamps a growing item by its maximum, and there is no such cap to hand Yoga. So it does not
|
|
651
|
+
* grow, and starts from its content, which is as far as the cap would let it get. It may still
|
|
652
|
+
* shrink.
|
|
653
|
+
*/
|
|
654
|
+
function withoutGrowth(props) {
|
|
655
|
+
if (typeof props['flex'] === 'number' && props['flex'] > 0) {
|
|
656
|
+
delete props['flex'];
|
|
657
|
+
props['flexShrink'] ??= 1;
|
|
658
|
+
}
|
|
659
|
+
delete props['flexGrow'];
|
|
660
|
+
delete props['flexBasis'];
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* `max-width: max-content`, and the same of a height: no bigger than its content, which for a
|
|
664
|
+
* box that would fill its container is `fit-content`. Yoga has no such cap. Answers whether the
|
|
665
|
+
* box is the size of its content for it: one that would fill, or one that was `fit-content` as
|
|
666
|
+
* written.
|
|
667
|
+
*
|
|
668
|
+
* ponytail: a size in points is left as it is, since nothing here knows how big the content
|
|
669
|
+
* is. Measure it if a box is ever given both.
|
|
670
|
+
*/
|
|
671
|
+
function capAtContent(props, size, cap) {
|
|
672
|
+
if (props[cap] !== 'max-content')
|
|
673
|
+
return false;
|
|
674
|
+
delete props[cap];
|
|
675
|
+
const own = props[size];
|
|
676
|
+
const fills = own == null || own === 'auto' || (typeof own === 'string' && own.endsWith('%'));
|
|
677
|
+
if (fills)
|
|
678
|
+
props[size] = 'fit-content';
|
|
679
|
+
return fills || own === 'fit-content';
|
|
680
|
+
}
|
|
526
681
|
const isPercent = (value) => typeof value === 'string' && value.endsWith('%');
|
|
682
|
+
/** Whether a `flex-basis` is a length or a percentage of something: not zero, and not `auto`. */
|
|
683
|
+
const isSizedBasis = (basis) => isPercent(basis)
|
|
684
|
+
? Number.parseFloat(basis) !== 0
|
|
685
|
+
: typeof basis === 'number' && basis !== 0;
|
|
686
|
+
/**
|
|
687
|
+
* A `flex-basis` that is a length or a percentage, as the size it is along its container's main
|
|
688
|
+
* axis: a width in a row and a height in a column, over one written for that axis.
|
|
689
|
+
*
|
|
690
|
+
* Yoga works a `flexBasis` out once for a view and keeps the answer, and React Native never has
|
|
691
|
+
* it forgotten when the view's props change (`computedFlexBasis`, cleared only by
|
|
692
|
+
* `markDirtyAndPropagate`). A basis that changes after the first layout is not read again, and
|
|
693
|
+
* a percentage does not follow the box it is a share of. A size is worked out on every pass, and
|
|
694
|
+
* with no basis Yoga takes the size along the main axis as the basis: the same sum.
|
|
695
|
+
*
|
|
696
|
+
* A basis of zero is left as it is, which is every `flex: 1`: under a container with no size on
|
|
697
|
+
* its main axis Yoga passes over a basis and sizes the box by its content, where a size of zero
|
|
698
|
+
* would be zero. A box out of the flow is no flex item, and keeps its own size.
|
|
699
|
+
*/
|
|
700
|
+
function basisAsSize(node, props) {
|
|
701
|
+
const basis = props['flexBasis'];
|
|
702
|
+
if (!isSizedBasis(basis) || props['position'] === 'absolute')
|
|
703
|
+
return;
|
|
704
|
+
const parent = layoutParent(node);
|
|
705
|
+
if (!parent)
|
|
706
|
+
return;
|
|
707
|
+
const container = containerOf(parent);
|
|
708
|
+
// Remembered on the container, so the box is merged again when the direction changes.
|
|
709
|
+
parent.fitContainer = container;
|
|
710
|
+
delete props['flexBasis'];
|
|
711
|
+
if (container.startsWith('row'))
|
|
712
|
+
props['width'] = basis;
|
|
713
|
+
else {
|
|
714
|
+
props['height'] = basis;
|
|
715
|
+
percentHeight(node, props);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
527
718
|
/**
|
|
528
719
|
* Whether a box has a height a percentage can be taken of, as CSS defines one: a height of its
|
|
529
720
|
* own, or one the layout around it gives it. A box as tall as what it holds has none.
|
|
@@ -558,14 +749,90 @@ function sizesItself(node, height) {
|
|
|
558
749
|
function heightFromLayout(node, parent) {
|
|
559
750
|
const [direction, align] = containerOf(parent).split(' ');
|
|
560
751
|
if (direction === 'row') {
|
|
561
|
-
// Stretched across a row, it is as tall as the row.
|
|
752
|
+
// Stretched across a row, it is as tall as the row, where the row has a height to be. One
|
|
753
|
+
// as tall as its content gives none: Yoga takes the percentage of the space on offer there
|
|
754
|
+
// too, the screen's, where a browser takes it of the row once its content has sized it.
|
|
562
755
|
const self = ownLayout(node, 'alignSelf');
|
|
563
|
-
return STRETCHES.has(self) && align === 'stretch';
|
|
756
|
+
return STRETCHES.has(self) && align === 'stretch' && definiteHeight(parent);
|
|
564
757
|
}
|
|
565
758
|
// In a column it has the height it grows to, where the column has one to share out.
|
|
566
759
|
const grows = Number(ownLayout(node, 'flexGrow') ?? ownLayout(node, 'flex') ?? 0) > 0;
|
|
567
760
|
return grows && definiteHeight(parent);
|
|
568
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* A placeholder's opacity, which native has none for, as its colour faded by that share: none of
|
|
764
|
+
* it at 0, which is how a stylesheet hides a placeholder under a label, and all of it at 1.
|
|
765
|
+
*
|
|
766
|
+
* ponytail: with no colour of its own the placeholder is native's default, and a platform colour
|
|
767
|
+
* is native's to resolve: neither is known here, so each is hidden at 0 and left as it is above
|
|
768
|
+
* that. Name the platform's default, or read the colour back, if one is faded part of the way.
|
|
769
|
+
*/
|
|
770
|
+
function placeholderFaded(props) {
|
|
771
|
+
const opacity = props['placeholderOpacity'];
|
|
772
|
+
if (opacity === undefined)
|
|
773
|
+
return;
|
|
774
|
+
delete props['placeholderOpacity'];
|
|
775
|
+
if (typeof opacity !== 'number' || opacity >= 1)
|
|
776
|
+
return;
|
|
777
|
+
const colour = props['placeholderTextColor'];
|
|
778
|
+
if (typeof colour === 'string')
|
|
779
|
+
props['placeholderTextColor'] = faded(colour, Math.max(opacity, 0));
|
|
780
|
+
else if (opacity <= 0)
|
|
781
|
+
props['placeholderTextColor'] = 'rgba(0, 0, 0, 0)';
|
|
782
|
+
}
|
|
783
|
+
/** The least alpha iOS sends a touch to a view at: under a hundredth it is passed over. */
|
|
784
|
+
const TOUCHED_ALPHA = 0.011;
|
|
785
|
+
/**
|
|
786
|
+
* A view that takes a touch and has no opacity, committed with the least there is to be sent
|
|
787
|
+
* one. A browser sends a press to an element at `opacity: 0`, which is how a see-through
|
|
788
|
+
* backdrop hears a press outside a menu; iOS passes over a view it would not draw. A hundredth
|
|
789
|
+
* of what the view draws is not seen.
|
|
790
|
+
*/
|
|
791
|
+
function stillTouched(props) {
|
|
792
|
+
// One told to take no touch is not to be sent one, seen or not.
|
|
793
|
+
if (props['pointerEvents'] === 'none')
|
|
794
|
+
return;
|
|
795
|
+
const opacity = props['opacity'];
|
|
796
|
+
if (typeof opacity === 'number' && opacity < TOUCHED_ALPHA)
|
|
797
|
+
props['opacity'] = TOUCHED_ALPHA;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
|
|
801
|
+
* for a size that is `auto`; Yoga takes it over one of two that are given, the height in a
|
|
802
|
+
* column and the width in a row.
|
|
803
|
+
*/
|
|
804
|
+
function ratioForAutoSize(props) {
|
|
805
|
+
if (!isSet(props['aspectRatio']))
|
|
806
|
+
return;
|
|
807
|
+
const given = (size) => isSet(size) && size !== 'auto';
|
|
808
|
+
if (given(props['width']) && given(props['height']))
|
|
809
|
+
delete props['aspectRatio'];
|
|
810
|
+
}
|
|
811
|
+
const OUTLINE_KEYS = ['outlineWidth', 'outlineStyle', 'outlineColor', 'outlineOffset'];
|
|
812
|
+
/**
|
|
813
|
+
* Send nothing for an outline of no width, which is what `outline: none` is: it draws nothing,
|
|
814
|
+
* and a view with no outline is the same view. Sent, it is an update that is not one, and on
|
|
815
|
+
* Android a text field given an outline prop after it is first drawn has its background set
|
|
816
|
+
* again, with which the padding Android gives a text field comes back over the padding it was
|
|
817
|
+
* laid out with: a field as tall as its line clips its text away. A stylesheet takes a
|
|
818
|
+
* browser's focus ring off a field with `outline: none` for the focused field alone.
|
|
819
|
+
*/
|
|
820
|
+
function noOutline(props) {
|
|
821
|
+
if (props['outlineWidth'] !== 0)
|
|
822
|
+
return;
|
|
823
|
+
for (const key of OUTLINE_KEYS)
|
|
824
|
+
delete props[key];
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* A box with `display: none`, out of the flow as well. It takes no room either way, but Yoga
|
|
828
|
+
* reads an item's baseline from its first child that is in the flow, and a hidden one has no
|
|
829
|
+
* layout to read: under a row with `align-items: baseline` the baseline is not a number, and
|
|
830
|
+
* the row comes to no height with nothing in it placed. Out of the flow it is passed over.
|
|
831
|
+
*/
|
|
832
|
+
function hiddenOutOfFlow(props) {
|
|
833
|
+
if (props['display'] === 'none')
|
|
834
|
+
props['position'] = 'absolute';
|
|
835
|
+
}
|
|
569
836
|
/**
|
|
570
837
|
* `height: 50%` under a parent with no height to take it of, which CSS reads as `auto`. Yoga
|
|
571
838
|
* takes it of the space on offer, the screen's or everything a scroll view holds, so no height
|
|
@@ -574,6 +841,9 @@ function heightFromLayout(node, parent) {
|
|
|
574
841
|
function percentHeight(node, props) {
|
|
575
842
|
if (!isPercent(props['height']))
|
|
576
843
|
return;
|
|
844
|
+
// Out of the flow, it is of the box that holds it, which is laid out first and has a height.
|
|
845
|
+
if (props['position'] === 'absolute')
|
|
846
|
+
return;
|
|
577
847
|
const parent = layoutParent(node);
|
|
578
848
|
if (parent && !definiteHeight(parent))
|
|
579
849
|
delete props['height'];
|
|
@@ -623,6 +893,43 @@ const TEXT_ALIGN = {
|
|
|
623
893
|
};
|
|
624
894
|
/** The native modal host, which is left out of a commit while `visible` is false. */
|
|
625
895
|
const MODAL_HOST = 'ModalHostView';
|
|
896
|
+
/**
|
|
897
|
+
* A screen's `gestureResponseDistance` that no touch is within: how far from each edge a swipe
|
|
898
|
+
* back may start, with -1 for no limit, and none at all from the leading edge. It is what
|
|
899
|
+
* `react-native-screens` reads as the swipe is about to begin; whether the swipe is enabled it
|
|
900
|
+
* read when the finger came down, before anything here had heard of the touch.
|
|
901
|
+
*/
|
|
902
|
+
const NO_SWIPE = Object.freeze({ start: -1, end: 0, top: -1, bottom: -1 });
|
|
903
|
+
/** A screen of a native stack, which is what the swipe back is a gesture of. */
|
|
904
|
+
const STACK_SCREEN = 'RNSScreen';
|
|
905
|
+
/** What an element's `touch-action` is: bound on it, or from the rules it matches. */
|
|
906
|
+
function touchActionOf(node) {
|
|
907
|
+
return inlineOf(node)['touchAction'] ?? node.styleCache?.style['touchAction'];
|
|
908
|
+
}
|
|
909
|
+
/** Where a touch is on the screen: its own point, or its first finger's. */
|
|
910
|
+
function pointOf(nativeEvent) {
|
|
911
|
+
const event = (nativeEvent ?? {});
|
|
912
|
+
const touch = event.pageX === undefined ? event.touches?.[0] : event;
|
|
913
|
+
return { x: touch?.pageX ?? 0, y: touch?.pageY ?? 0 };
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* What a touch on a node leaves to the browser: the `touch-action` of it, or of the nearest
|
|
917
|
+
* element over it that says. Nothing where none does, which leaves everything.
|
|
918
|
+
*/
|
|
919
|
+
function touchActions(node) {
|
|
920
|
+
for (let up = node; up; up = up.parent) {
|
|
921
|
+
const said = up.kind === 'element' ? touchActionOf(up) : undefined;
|
|
922
|
+
if (typeof said === 'string')
|
|
923
|
+
return said.split(' ');
|
|
924
|
+
}
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
const PANS_ACROSS = ['auto', 'manipulation', 'pan-x', 'pan-left', 'pan-right'];
|
|
928
|
+
const PANS_ALONG = ['auto', 'manipulation', 'pan-y', 'pan-up', 'pan-down'];
|
|
929
|
+
/** Whether an element keeps a drag in one direction: its `touch-action` leaves no pan that way. */
|
|
930
|
+
const keeps = (actions, pans) => actions !== undefined && !actions.some((action) => pans.includes(action));
|
|
931
|
+
/** How far a finger moves before a browser has settled which way a drag is going. */
|
|
932
|
+
const DRAG_SLOP = 4;
|
|
626
933
|
/**
|
|
627
934
|
* Register a codegen'd third-party Fabric component, e.g.
|
|
628
935
|
* `registerViewName('rns-screen', 'RNSScreen')`, which is how the router reaches
|
|
@@ -881,12 +1188,14 @@ function isTextElement(node) {
|
|
|
881
1188
|
return false;
|
|
882
1189
|
return !TEXT_ELEMENTS.has(node.name) || holdsOnlyText(node);
|
|
883
1190
|
}
|
|
1191
|
+
/** Whether a text element holds text and nothing else. One that holds nothing holds no text. */
|
|
884
1192
|
function holdsOnlyText(node) {
|
|
885
|
-
|
|
1193
|
+
const children = node.children ?? [];
|
|
1194
|
+
for (const child of children) {
|
|
886
1195
|
if (child.kind === 'element' && !isTextElement(child))
|
|
887
1196
|
return false;
|
|
888
1197
|
}
|
|
889
|
-
return
|
|
1198
|
+
return children.length > 0;
|
|
890
1199
|
}
|
|
891
1200
|
const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
|
|
892
1201
|
/**
|
|
@@ -896,20 +1205,26 @@ const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
|
|
|
896
1205
|
* A browser makes the run a flex item and places it. A paragraph's text is its content, which no
|
|
897
1206
|
* alignment moves, so such an element commits as a view, and its text as the paragraph any view's
|
|
898
1207
|
* loose text is. Read from the node's own style on each reconcile, which a change to it brings
|
|
899
|
-
* about, so nothing is kept to go stale.
|
|
1208
|
+
* about, so nothing is kept to go stale. The run can be a text element of its own, a label in a
|
|
1209
|
+
* span, which is then the item placed. A `<text>` is a paragraph whatever its style, and an
|
|
900
1210
|
* element holding more than the one run is left as it was.
|
|
901
1211
|
*/
|
|
902
1212
|
function aligningText(node) {
|
|
903
1213
|
if (node.kind !== 'element' || node.name === 'text' || !TEXT_ELEMENTS.has(node.name)) {
|
|
904
1214
|
return false;
|
|
905
1215
|
}
|
|
906
|
-
if (node.children.length !== 1 || node.children[0]
|
|
1216
|
+
if (node.children.length !== 1 || !oneRun(node.children[0]))
|
|
1217
|
+
return false;
|
|
1218
|
+
// Under a text element it is a span of that paragraph, unless that one is a view placing it.
|
|
1219
|
+
if (isTextElement(node.parent) && !aligningText(node.parent))
|
|
907
1220
|
return false;
|
|
908
|
-
if (
|
|
1221
|
+
if (ownLayout(node, 'display') !== 'flex')
|
|
909
1222
|
return false;
|
|
910
1223
|
return (ALIGNS.has(ownLayout(node, 'alignItems')) ||
|
|
911
1224
|
ALIGNS.has(ownLayout(node, 'justifyContent')));
|
|
912
1225
|
}
|
|
1226
|
+
/** One run of text to place: text itself, or a text element around some, a label in a span. */
|
|
1227
|
+
const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.name));
|
|
913
1228
|
/** The view a node is committed as: `viewNameOf`, but for a text element that aligns its text. */
|
|
914
1229
|
const committedViewName = (node) => aligningText(node) ? DEFAULT_VIEW : viewNameOf(node);
|
|
915
1230
|
/**
|
|
@@ -940,8 +1255,15 @@ export function viewNameOf(node) {
|
|
|
940
1255
|
}
|
|
941
1256
|
// The paragraph the engine makes for a view's loose text is under a view, whatever the name
|
|
942
1257
|
// of the element that view is.
|
|
943
|
-
return
|
|
1258
|
+
return nestedText(node) ? VIRTUAL_TEXT : PARAGRAPH;
|
|
944
1259
|
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Whether a text element is a span of the paragraph above it: under another text element, but
|
|
1262
|
+
* not one that is a view for placing this one (see `aligningText`), nor the engine's own box.
|
|
1263
|
+
*/
|
|
1264
|
+
const nestedText = (node) => !node.anonymous && isTextElement(node.parent) && !placesText(node.parent);
|
|
1265
|
+
/** Whether a node's parent is a text element committed as a view, to place the run it holds. */
|
|
1266
|
+
const placesText = (parent) => parent != null && 'props' in parent && aligningText(parent);
|
|
945
1267
|
/**
|
|
946
1268
|
* The props every native view reads as a boolean: the `bool` fields of React Native's
|
|
947
1269
|
* `BaseViewProps`, `AccessibilityProps` and `HostPlatformViewProps`, by the names JavaScript sends.
|
|
@@ -980,16 +1302,12 @@ const BOOLEAN_VIEW_PROPS = new Set([
|
|
|
980
1302
|
]);
|
|
981
1303
|
/**
|
|
982
1304
|
* Whether `node` holds nothing `:empty` sees besides `moved`: so it was empty before `moved` came,
|
|
983
|
-
* or is now that it has gone.
|
|
1305
|
+
* or is now that it has gone. What `:empty` does not see is `fills` in css.ts to say.
|
|
984
1306
|
*/
|
|
985
1307
|
function wasOrIsEmptyWithout(node, moved) {
|
|
986
|
-
if (moved
|
|
1308
|
+
if (!fills(moved))
|
|
987
1309
|
return false;
|
|
988
|
-
|
|
989
|
-
if (child !== moved && child.kind !== 'anchor')
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
return true;
|
|
1310
|
+
return !node.children.some((child) => child !== moved && fills(child));
|
|
993
1311
|
}
|
|
994
1312
|
/**
|
|
995
1313
|
* Have the child that paints a node's background merged again with the node: nothing of the
|
|
@@ -1556,6 +1874,16 @@ const now = () => globalThis.performance?.now?.() ?? Date.now();
|
|
|
1556
1874
|
function sameHandles(a, b) {
|
|
1557
1875
|
return a.length === b.length && a.every((handle, i) => handle === b[i]);
|
|
1558
1876
|
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Whether a node could be the element a compound is written for, by its classes alone: its own,
|
|
1879
|
+
* and those of one alternative of each `:is()` on it, which is where Tailwind names a group or
|
|
1880
|
+
* a peer. A compound with no class in either could be any element.
|
|
1881
|
+
*/
|
|
1882
|
+
function couldBe(compound, node) {
|
|
1883
|
+
if (!compound.classes.every((name) => node.classes?.has(name)))
|
|
1884
|
+
return false;
|
|
1885
|
+
return (compound.is ?? []).every((alternatives) => alternatives.some((option) => couldBe(option, node)));
|
|
1886
|
+
}
|
|
1559
1887
|
const FRAME_BUDGET_MS = 8;
|
|
1560
1888
|
export class Engine {
|
|
1561
1889
|
/** Scopes the CSS resolver's memo. Bumped once per commit. */
|
|
@@ -1612,6 +1940,14 @@ export class Engine {
|
|
|
1612
1940
|
* does, a child list can move without anything else needing to be looked at again.
|
|
1613
1941
|
*/
|
|
1614
1942
|
structuralSheets = false;
|
|
1943
|
+
/** How many children after a change in a list one of them reaches. See `siblingReach`. */
|
|
1944
|
+
structuralAfter = 0;
|
|
1945
|
+
/** Whether one of them reaches the children before it. */
|
|
1946
|
+
structuralBefore = false;
|
|
1947
|
+
/** The compounds of theirs that read an element's place from under it. See `markPlace`. */
|
|
1948
|
+
placeElsewhere = new Set();
|
|
1949
|
+
/** The sheets `watchStructure` has read, which it is asked about once an element. */
|
|
1950
|
+
watchedStructure = new WeakSet();
|
|
1615
1951
|
/** Whether any sheet uses `:has()`. See `markBeneath`. */
|
|
1616
1952
|
hasSheets = false;
|
|
1617
1953
|
resolveAssetSource;
|
|
@@ -1624,7 +1960,7 @@ export class Engine {
|
|
|
1624
1960
|
this.fontScale = conditions.fontScale;
|
|
1625
1961
|
if (options.tokens)
|
|
1626
1962
|
this.styles.setRootTokens(options.tokens);
|
|
1627
|
-
this.
|
|
1963
|
+
this.watchStructure(options.globalStyles);
|
|
1628
1964
|
this.watchHas(options.globalStyles);
|
|
1629
1965
|
this.watchActive(options.globalStyles);
|
|
1630
1966
|
this.dev = options.dev ?? globalThis.__DEV__ === true;
|
|
@@ -1748,9 +2084,7 @@ export class Engine {
|
|
|
1748
2084
|
const { own, elsewhere } = this.activeUse;
|
|
1749
2085
|
if (!own && !elsewhere.length)
|
|
1750
2086
|
return;
|
|
1751
|
-
|
|
1752
|
-
const asked = elsewhere.some((compound) => compound.classes.every((name) => node.classes?.has(name)));
|
|
1753
|
-
if (asked)
|
|
2087
|
+
if (elsewhere.some((compound) => couldBe(compound, node)))
|
|
1754
2088
|
return this.markProps(node);
|
|
1755
2089
|
node.stateDirty = true;
|
|
1756
2090
|
this.markPath(node.parent ?? node);
|
|
@@ -1775,13 +2109,26 @@ export class Engine {
|
|
|
1775
2109
|
this.sheetReplaced(replacing, sheet);
|
|
1776
2110
|
else
|
|
1777
2111
|
this.registerSheet(sheet);
|
|
1778
|
-
|
|
1779
|
-
this.structuralSheets = true;
|
|
2112
|
+
this.watchStructure(sheet);
|
|
1780
2113
|
this.watchHas(sheet);
|
|
1781
2114
|
this.watchActive(sheet);
|
|
1782
2115
|
this.markPath(this.root);
|
|
2116
|
+
this.markWrittenFor(sheet, this.root);
|
|
1783
2117
|
return true;
|
|
1784
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Have styled again each node a sheet just added could match. The rest keep the style they
|
|
2121
|
+
* have, which the resolver lets stand for a node none of the sheet's rules is written for,
|
|
2122
|
+
* so a commit steps over them: the walk here is what finds the ones it must not step over.
|
|
2123
|
+
*/
|
|
2124
|
+
markWrittenFor(sheet, node) {
|
|
2125
|
+
if (node.kind === 'element' && this.styles.couldMatch(sheet, node)) {
|
|
2126
|
+
node.styleDirty = true;
|
|
2127
|
+
this.markPath(node);
|
|
2128
|
+
}
|
|
2129
|
+
for (const child of node.children)
|
|
2130
|
+
this.markWrittenFor(sheet, child);
|
|
2131
|
+
}
|
|
1785
2132
|
/**
|
|
1786
2133
|
* Stop matching a sheet `addGlobalSheet` added, and restyle everything; false, and nothing
|
|
1787
2134
|
* done, when it is not one. A hot swap that leaves a None component no rules, or makes it scoped.
|
|
@@ -1967,8 +2314,8 @@ export class Engine {
|
|
|
1967
2314
|
if (sheet === undefined || node.sheet === sheet)
|
|
1968
2315
|
return;
|
|
1969
2316
|
node.sheet = sheet;
|
|
1970
|
-
|
|
1971
|
-
|
|
2317
|
+
this.styles.noteSheet(sheet);
|
|
2318
|
+
this.watchStructure(sheet);
|
|
1972
2319
|
this.watchHas(sheet);
|
|
1973
2320
|
this.watchActive(sheet);
|
|
1974
2321
|
this.markProps(node);
|
|
@@ -2003,14 +2350,14 @@ export class Engine {
|
|
|
2003
2350
|
createElement(name, sheet = null) {
|
|
2004
2351
|
const node = new RetainedNode('element', name, this);
|
|
2005
2352
|
node.sheet = sheet;
|
|
2353
|
+
this.styles.noteSheet(sheet);
|
|
2006
2354
|
const html = HTML_ELEMENTS.get(name);
|
|
2007
2355
|
if (html !== undefined) {
|
|
2008
2356
|
if (html.role)
|
|
2009
2357
|
node.props['accessibilityRole'] = html.role;
|
|
2010
2358
|
node.styled = html.styled;
|
|
2011
2359
|
}
|
|
2012
|
-
|
|
2013
|
-
this.structuralSheets = true;
|
|
2360
|
+
this.watchStructure(sheet);
|
|
2014
2361
|
this.watchHas(sheet);
|
|
2015
2362
|
this.watchActive(sheet);
|
|
2016
2363
|
if (HOISTS[name])
|
|
@@ -2019,11 +2366,45 @@ export class Engine {
|
|
|
2019
2366
|
}
|
|
2020
2367
|
addClass(node, name) {
|
|
2021
2368
|
(node.classes ??= new Set()).add(name);
|
|
2022
|
-
this.
|
|
2369
|
+
this.markClasses(node, [name]);
|
|
2023
2370
|
}
|
|
2024
2371
|
removeClass(node, name) {
|
|
2025
2372
|
node.classes?.delete(name);
|
|
2026
|
-
this.
|
|
2373
|
+
this.markClasses(node, [name]);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Mark a node whose classes changed, by what the rules that name those classes can restyle.
|
|
2377
|
+
*
|
|
2378
|
+
* A class no selector names changes no style: a library's own marker, of an overlay that is
|
|
2379
|
+
* animating or a control that was touched. One named only as what a rule's element is
|
|
2380
|
+
* inside, `.animating .close`, restyles the elements under the node that such rules are for.
|
|
2381
|
+
* Either way a whole subtree is not styled again for a class that has no say in it. Any other
|
|
2382
|
+
* class restyles the node and everything under it, as any other change to it does.
|
|
2383
|
+
*
|
|
2384
|
+
* A sheet that names a class later restyles what it is for as it arrives, and a node not yet
|
|
2385
|
+
* committed is styled when it is, with the class as it is then.
|
|
2386
|
+
*/
|
|
2387
|
+
markClasses(node, changed) {
|
|
2388
|
+
const reach = this.styles.reachOf(changed);
|
|
2389
|
+
if (reach === true)
|
|
2390
|
+
return this.markProps(node);
|
|
2391
|
+
this.markProps(node, false);
|
|
2392
|
+
if (reach)
|
|
2393
|
+
this.markUnder(node, reach);
|
|
2394
|
+
}
|
|
2395
|
+
/** Have styled again each element under a node that one of `subjects`' rules could be for. */
|
|
2396
|
+
markUnder(node, subjects) {
|
|
2397
|
+
for (const child of node.children) {
|
|
2398
|
+
if (child.kind !== 'element')
|
|
2399
|
+
continue;
|
|
2400
|
+
if (this.styles.isFor(subjects, child)) {
|
|
2401
|
+
// And with it all under it, which is styled from it.
|
|
2402
|
+
child.styleDirty = true;
|
|
2403
|
+
this.markPath(child);
|
|
2404
|
+
}
|
|
2405
|
+
else
|
|
2406
|
+
this.markUnder(child, subjects);
|
|
2407
|
+
}
|
|
2027
2408
|
}
|
|
2028
2409
|
/**
|
|
2029
2410
|
* The sheet of the component `node` hosts, whose `:host` rules it matches. Set again when a hot
|
|
@@ -2034,6 +2415,8 @@ export class Engine {
|
|
|
2034
2415
|
if (node.hostSheet === sheet)
|
|
2035
2416
|
return;
|
|
2036
2417
|
node.hostSheet = sheet;
|
|
2418
|
+
this.styles.noteSheet(sheet);
|
|
2419
|
+
this.watchStructure(sheet);
|
|
2037
2420
|
this.watchActive(sheet);
|
|
2038
2421
|
this.markProps(node);
|
|
2039
2422
|
}
|
|
@@ -2049,8 +2432,14 @@ export class Engine {
|
|
|
2049
2432
|
/** `class="a b"` from a template. Replaces the set rather than adding to it. */
|
|
2050
2433
|
setClasses(node, value) {
|
|
2051
2434
|
const names = value.split(/\s+/).filter(Boolean);
|
|
2435
|
+
const before = node.classes;
|
|
2052
2436
|
node.classes = names.length ? new Set(names) : null;
|
|
2053
|
-
|
|
2437
|
+
// Each class that came or went: one in both changes nothing.
|
|
2438
|
+
const changed = [...names.filter((name) => !before?.has(name))];
|
|
2439
|
+
for (const name of before ?? [])
|
|
2440
|
+
if (!node.classes?.has(name))
|
|
2441
|
+
changed.push(name);
|
|
2442
|
+
this.markClasses(node, changed);
|
|
2054
2443
|
}
|
|
2055
2444
|
/** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
|
|
2056
2445
|
createAnchor() {
|
|
@@ -2067,7 +2456,7 @@ export class Engine {
|
|
|
2067
2456
|
parent.children.push(child);
|
|
2068
2457
|
if (child.dormantHoists)
|
|
2069
2458
|
this.wakeHoists(child);
|
|
2070
|
-
this.markStructure(parent, child);
|
|
2459
|
+
this.markStructure(parent, child, parent.children.length - 1);
|
|
2071
2460
|
this.markTextContent(parent, child);
|
|
2072
2461
|
}
|
|
2073
2462
|
insertBefore(parent, child, ref) {
|
|
@@ -2080,7 +2469,7 @@ export class Engine {
|
|
|
2080
2469
|
parent.children.splice(at, 0, child);
|
|
2081
2470
|
if (child.dormantHoists)
|
|
2082
2471
|
this.wakeHoists(child);
|
|
2083
|
-
this.markStructure(parent, child);
|
|
2472
|
+
this.markStructure(parent, child, at < 0 ? parent.children.length - 1 : at);
|
|
2084
2473
|
this.markTextContent(parent, child);
|
|
2085
2474
|
}
|
|
2086
2475
|
removeChild(parent, child) {
|
|
@@ -2093,7 +2482,7 @@ export class Engine {
|
|
|
2093
2482
|
target.children.splice(at, 1);
|
|
2094
2483
|
child.parent = null;
|
|
2095
2484
|
this.removedSinceCommit.add(child);
|
|
2096
|
-
this.markStructure(target, child);
|
|
2485
|
+
this.markStructure(target, child, at);
|
|
2097
2486
|
this.markTextContent(target, child);
|
|
2098
2487
|
}
|
|
2099
2488
|
/**
|
|
@@ -2233,7 +2622,9 @@ export class Engine {
|
|
|
2233
2622
|
// Inline style is applied after the cascade and inherited by nothing, so it cannot change
|
|
2234
2623
|
// what any node matches or inherits. Every other prop can: `[disabled]` is a selector. An
|
|
2235
2624
|
// inline `direction` is the exception, because the paragraphs below it align by it.
|
|
2236
|
-
this
|
|
2625
|
+
// And a child that takes one of this node's own values, an `inherit`, follows it.
|
|
2626
|
+
const heirs = node.styleCache?.heirs === true;
|
|
2627
|
+
this.markProps(node, key !== 'style' || this.inlineReachesStyle(node) || heirs);
|
|
2237
2628
|
}
|
|
2238
2629
|
/**
|
|
2239
2630
|
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
@@ -2331,10 +2722,21 @@ export class Engine {
|
|
|
2331
2722
|
this.markProps(node);
|
|
2332
2723
|
}
|
|
2333
2724
|
setText(node, value) {
|
|
2725
|
+
const filled = (node.text === '') !== (value === '');
|
|
2334
2726
|
node.text = value;
|
|
2335
2727
|
this.markProps(node);
|
|
2336
|
-
if (node.parent)
|
|
2337
|
-
|
|
2728
|
+
if (!node.parent)
|
|
2729
|
+
return;
|
|
2730
|
+
this.markTextContent(node.parent, node);
|
|
2731
|
+
// Text of no length is nothing to `:empty`: its first character and its last change what
|
|
2732
|
+
// the element it is in matches, and what comes after that element.
|
|
2733
|
+
if (filled && this.structuralSheets)
|
|
2734
|
+
this.markFilled(node.parent);
|
|
2735
|
+
}
|
|
2736
|
+
markFilled(node) {
|
|
2737
|
+
node.styleDirty = true;
|
|
2738
|
+
this.markLaterSiblings(node);
|
|
2739
|
+
this.markPath(node);
|
|
2338
2740
|
}
|
|
2339
2741
|
setEventListener(node, topLevelType, fn) {
|
|
2340
2742
|
this.registerEventHandler();
|
|
@@ -2398,24 +2800,101 @@ export class Engine {
|
|
|
2398
2800
|
siblings[i].styleDirty = true;
|
|
2399
2801
|
}
|
|
2400
2802
|
}
|
|
2401
|
-
/** `node`'s child list changed: `moved` came into it or went out of it. */
|
|
2402
|
-
markStructure(node, moved) {
|
|
2803
|
+
/** `node`'s child list changed: `moved` came into it at `at`, or went out of it from there. */
|
|
2804
|
+
markStructure(node, moved, at) {
|
|
2403
2805
|
node.structureDirty = true;
|
|
2404
2806
|
// A child list that moved changes what its members match, though nothing about them did:
|
|
2405
2807
|
// the old last row is no longer the last. Only sheets that ask about position pay for this.
|
|
2406
2808
|
if (this.structuralSheets) {
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
//
|
|
2411
|
-
//
|
|
2412
|
-
|
|
2809
|
+
moved.styleDirty = true;
|
|
2810
|
+
this.markRepositioned(node.children, at, moved);
|
|
2811
|
+
// What the node itself matches hangs on its child list only through `:empty`, which only
|
|
2812
|
+
// its first child arriving or its last one leaving changes: and then what a later sibling
|
|
2813
|
+
// matches can too, `.box:empty + .spacer`. Styled again for any other change, everything
|
|
2814
|
+
// under it would be resolved again with it.
|
|
2815
|
+
if (wasOrIsEmptyWithout(node, moved)) {
|
|
2816
|
+
node.styleDirty = true;
|
|
2413
2817
|
this.markLaterSiblings(node);
|
|
2818
|
+
}
|
|
2414
2819
|
}
|
|
2415
2820
|
if (this.hasSheets)
|
|
2416
2821
|
this.markBeneath(node);
|
|
2417
2822
|
this.markPath(node.parent);
|
|
2418
2823
|
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Mark the children whose place in a child list that changed at `at` a sheet could read
|
|
2826
|
+
* differently. The two at each end always, which are the old first and last and the new. Those
|
|
2827
|
+
* from `at` on where some sheet counts from the start or asks about any one before, since a
|
|
2828
|
+
* child's place from the start hangs only on those before it; and those before `at` where one
|
|
2829
|
+
* counts from the end. Styling one again resolves everything under it again, so a long list
|
|
2830
|
+
* gaining a row at its end is not the whole list.
|
|
2831
|
+
*
|
|
2832
|
+
* Where the sheets ask no further than the element just before (`+`), a rule steps a known
|
|
2833
|
+
* number of elements along from one whose own match changed, and those are all there is to
|
|
2834
|
+
* mark: that many after the change, and that many after the two at the start, either of which
|
|
2835
|
+
* may have stopped or started being first. Nothing comes after the two at the end.
|
|
2836
|
+
*/
|
|
2837
|
+
markRepositioned(children, at, moved) {
|
|
2838
|
+
const steps = this.structuralAfter;
|
|
2839
|
+
const pastFirst = steps === Infinity ? 0 : steps;
|
|
2840
|
+
for (const child of elementsAtEnd(children, 2 + pastFirst, false))
|
|
2841
|
+
this.markPlace(child);
|
|
2842
|
+
for (const child of elementsAtEnd(children, 2, true))
|
|
2843
|
+
this.markPlace(child);
|
|
2844
|
+
if (this.structuralBefore)
|
|
2845
|
+
for (let i = 0; i < at; i++)
|
|
2846
|
+
this.markPlace(children[i]);
|
|
2847
|
+
// The elements after the change, not counting the one that came, which is marked already.
|
|
2848
|
+
let left = steps;
|
|
2849
|
+
for (let i = at; i < children.length && left > 0; i++) {
|
|
2850
|
+
const child = children[i];
|
|
2851
|
+
this.markPlace(child);
|
|
2852
|
+
if (child.kind === 'element' && child !== moved)
|
|
2853
|
+
left--;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* A child's place in its list changed, which is all that changed about it. It is matched
|
|
2858
|
+
* again, and keeps its style and all that is under it unless it now matches other rules: its
|
|
2859
|
+
* place is read by the rules that style it and by no other. Not where a rule reads the place
|
|
2860
|
+
* of an element like it from under it, `.row:nth-child(odd) .label`: that changes more than
|
|
2861
|
+
* the element itself, which is then styled again with everything under it, as is a child
|
|
2862
|
+
* that is no element.
|
|
2863
|
+
*/
|
|
2864
|
+
markPlace(child) {
|
|
2865
|
+
if (child.kind !== 'element' || this.placeReadFromUnder(child))
|
|
2866
|
+
child.styleDirty = true;
|
|
2867
|
+
else
|
|
2868
|
+
child.stateDirty = true;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Whether a rule could read this element's place from another element. By its classes alone,
|
|
2872
|
+
* which every such compound it could be asks for: a compound with none could be any element.
|
|
2873
|
+
*/
|
|
2874
|
+
placeReadFromUnder(node) {
|
|
2875
|
+
for (const compound of this.placeElsewhere) {
|
|
2876
|
+
if (compound.classes.every((name) => node.classes?.has(name)))
|
|
2877
|
+
return true;
|
|
2878
|
+
}
|
|
2879
|
+
return false;
|
|
2880
|
+
}
|
|
2881
|
+
/** Note what a sheet coming into play asks about a child list. None is unset: see `structural`. */
|
|
2882
|
+
watchStructure(sheet) {
|
|
2883
|
+
if (!sheet?.structural || this.watchedStructure.has(sheet))
|
|
2884
|
+
return;
|
|
2885
|
+
this.watchedStructure.add(sheet);
|
|
2886
|
+
this.structuralSheets = true;
|
|
2887
|
+
const reach = siblingReach(sheet);
|
|
2888
|
+
this.structuralAfter = Math.max(this.structuralAfter, reach.after);
|
|
2889
|
+
if (reach.before)
|
|
2890
|
+
this.structuralBefore = true;
|
|
2891
|
+
for (const compound of placeReadElsewhere(sheet))
|
|
2892
|
+
this.placeElsewhere.add(compound);
|
|
2893
|
+
// What a child whose place changed is compared with is the rules it matched, which a cache
|
|
2894
|
+
// keeps from here on. One made before has none, and is styled again the first time, as it
|
|
2895
|
+
// was before any sheet asked.
|
|
2896
|
+
this.styles.tracksHas = true;
|
|
2897
|
+
}
|
|
2419
2898
|
/** Walk up until we hit a node already known to have a dirty subtree. */
|
|
2420
2899
|
markPath(node) {
|
|
2421
2900
|
const wasClean = !this.root.structureDirty && !this.root.subtreeDirty;
|
|
@@ -2461,6 +2940,7 @@ export class Engine {
|
|
|
2461
2940
|
this.fabric.appendChildToSet(set, this.reconcileUnder(this.root, child));
|
|
2462
2941
|
}
|
|
2463
2942
|
this.clearFlags(this.root);
|
|
2943
|
+
this.announceCommit();
|
|
2464
2944
|
this.fabric.completeRoot(this.rootTag, set);
|
|
2465
2945
|
const elapsed = now() - started;
|
|
2466
2946
|
this.stats.commits++;
|
|
@@ -2475,6 +2955,7 @@ export class Engine {
|
|
|
2475
2955
|
this.stats.slowCommits++;
|
|
2476
2956
|
}
|
|
2477
2957
|
this.flushTransitionEvents();
|
|
2958
|
+
this.playSized();
|
|
2478
2959
|
this.scrollDriver?.afterCommit();
|
|
2479
2960
|
// Read first: the commit that settles keyframes starts by clearing it.
|
|
2480
2961
|
const facesAdded = this.facesAdded;
|
|
@@ -2484,6 +2965,25 @@ export class Engine {
|
|
|
2484
2965
|
this.rematchFonts();
|
|
2485
2966
|
return true;
|
|
2486
2967
|
}
|
|
2968
|
+
beforeCommit = null;
|
|
2969
|
+
announceCommit() {
|
|
2970
|
+
const run = this.beforeCommit;
|
|
2971
|
+
this.beforeCommit = null;
|
|
2972
|
+
run?.();
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Run `run` once, as the next commit hands its tree to the host and before the host has it: the
|
|
2976
|
+
* moment for something the host applies to its next commit, which is only this one from here.
|
|
2977
|
+
* A pass that changes nothing commits nothing and runs nothing, so a request waits until it is
|
|
2978
|
+
* taken back with the function returned. A later request replaces an earlier one.
|
|
2979
|
+
*/
|
|
2980
|
+
beforeNextCommit(run) {
|
|
2981
|
+
this.beforeCommit = run;
|
|
2982
|
+
return () => {
|
|
2983
|
+
if (this.beforeCommit === run)
|
|
2984
|
+
this.beforeCommit = null;
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2487
2987
|
/** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
|
|
2488
2988
|
unseen = Infinity;
|
|
2489
2989
|
/**
|
|
@@ -2588,11 +3088,36 @@ export class Engine {
|
|
|
2588
3088
|
if (this.transitionEvents.length === 0)
|
|
2589
3089
|
return;
|
|
2590
3090
|
for (const { node, type, property } of this.transitionEvents.splice(0)) {
|
|
3091
|
+
// One listener that throws stops neither the rest nor the node being let go.
|
|
2591
3092
|
for (const listener of [...(node.listeners?.get(type) ?? [])]) {
|
|
2592
|
-
|
|
3093
|
+
try {
|
|
3094
|
+
listener(animationEvent(type, node, property));
|
|
3095
|
+
}
|
|
3096
|
+
catch (error) {
|
|
3097
|
+
this.reportEventError(error, type);
|
|
3098
|
+
}
|
|
2593
3099
|
}
|
|
3100
|
+
if (type === 'topAnimationend')
|
|
3101
|
+
this.letGo(node);
|
|
2594
3102
|
}
|
|
2595
3103
|
}
|
|
3104
|
+
/**
|
|
3105
|
+
* The nodes whose animation has ended with no fill and is still held at its last frame, until
|
|
3106
|
+
* its `animationend` has been heard. A browser runs that listener before it paints again, and
|
|
3107
|
+
* an exit animation's listener takes the element away: it is never seen back at rest. Here the
|
|
3108
|
+
* listener runs after the commit, so the commit that ends the animation paints the last frame,
|
|
3109
|
+
* and the element goes back to rest once the listener has run and left it there.
|
|
3110
|
+
*/
|
|
3111
|
+
lettingGo = new Set();
|
|
3112
|
+
letGo(node) {
|
|
3113
|
+
if (!this.lettingGo.delete(node))
|
|
3114
|
+
return;
|
|
3115
|
+
const running = node.playing;
|
|
3116
|
+
if (!running?.done)
|
|
3117
|
+
return;
|
|
3118
|
+
running.values = {};
|
|
3119
|
+
this.markProps(node, false);
|
|
3120
|
+
}
|
|
2596
3121
|
/** Anchors take part in sibling ordering but never reach Fabric. */
|
|
2597
3122
|
visibleChildren(node) {
|
|
2598
3123
|
// A view that takes its text as a prop has no text children to commit. See `ViewNameOptions`.
|
|
@@ -2602,13 +3127,21 @@ export class Engine {
|
|
|
2602
3127
|
/**
|
|
2603
3128
|
* Everything a node renders with, unprocessed. Precedence, weakest first: native defaults, the
|
|
2604
3129
|
* node's `defaultStyle`, matched CSS, explicit props, inline style, a component's
|
|
2605
|
-
* `styleOverride`. Inline wins over CSS
|
|
3130
|
+
* `styleOverride`. Inline wins over CSS as it does on the web, but for a declaration marked
|
|
3131
|
+
* `!important`, which stands over it: see `inlineOf`.
|
|
2606
3132
|
*
|
|
2607
3133
|
* Raw, because this is what the next commit diffs against. Colours and asset ids are converted
|
|
2608
3134
|
* on the way out instead (`processed`), and only for the keys that changed: the converters
|
|
2609
3135
|
* return fresh objects, so diffing their output would re-send every image and shadow on every
|
|
2610
3136
|
* unrelated change.
|
|
2611
3137
|
*/
|
|
3138
|
+
/** What a touch on an element that keeps a drag holds off, while it lasts: see `holdSwipe`. */
|
|
3139
|
+
heldFor(node, merged) {
|
|
3140
|
+
if (node === this.swipeHeld)
|
|
3141
|
+
merged['gestureResponseDistance'] = NO_SWIPE;
|
|
3142
|
+
if (node === this.scrollHeld)
|
|
3143
|
+
merged['scrollEnabled'] = false;
|
|
3144
|
+
}
|
|
2612
3145
|
mergeProps(node, viewName) {
|
|
2613
3146
|
if (node.kind === 'text')
|
|
2614
3147
|
return { text: paragraphText(node) };
|
|
@@ -2622,14 +3155,15 @@ export class Engine {
|
|
|
2622
3155
|
writeOwnProps(node.props, props, node.attributeOnly);
|
|
2623
3156
|
withTextContent(node, viewName, props);
|
|
2624
3157
|
const cascaded = props['transform'];
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3158
|
+
const inline = inlineOf(node);
|
|
3159
|
+
const style = boundTransform(node, Object.assign(props, inline), cascaded);
|
|
3160
|
+
if (inline !== NO_STYLE)
|
|
3161
|
+
this.styles.overOtherForms(inline, style);
|
|
2629
3162
|
nativePointerEvents(node, style, resolved);
|
|
2630
3163
|
// Before an image's own size: `fit-content` is no size, so the image's is what it gets.
|
|
2631
3164
|
fitContent(node, style);
|
|
2632
3165
|
percentHeight(node, style);
|
|
3166
|
+
placeholderFaded(style);
|
|
2633
3167
|
const intrinsic = node.props[INTRINSIC_SIZE];
|
|
2634
3168
|
if (intrinsic)
|
|
2635
3169
|
applyIntrinsicSize(style, intrinsic);
|
|
@@ -2641,8 +3175,24 @@ export class Engine {
|
|
|
2641
3175
|
if (this.fontsRefreshed)
|
|
2642
3176
|
this.capForFonts(node, style);
|
|
2643
3177
|
alignMultiline(viewName, style);
|
|
3178
|
+
rowsTall(style, this.fontScale);
|
|
2644
3179
|
const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
2645
|
-
|
|
3180
|
+
// On iOS: Android's field says the baseline of the line it is sized and centred by.
|
|
3181
|
+
// Still under the row that marked it: a row taken away is not committed again to unmark it.
|
|
3182
|
+
const row = node.onBaseline;
|
|
3183
|
+
const onBaseline = viewName === 'TextInput' && row !== undefined && isWithin(node, row);
|
|
3184
|
+
centreSingleLine(viewName, merged, this.fontScale, onBaseline);
|
|
3185
|
+
// After a transition, which eases the basis as the basis it was written as.
|
|
3186
|
+
basisAsSize(node, merged);
|
|
3187
|
+
// After the basis, which is a size given where it is committed as one.
|
|
3188
|
+
ratioForAutoSize(merged);
|
|
3189
|
+
noOutline(merged);
|
|
3190
|
+
// Last, on what is committed: an override or an animation can hide a box, or place it.
|
|
3191
|
+
hiddenOutOfFlow(merged);
|
|
3192
|
+
delete merged['touchAction'];
|
|
3193
|
+
if (this.responders.has(node))
|
|
3194
|
+
stillTouched(merged);
|
|
3195
|
+
this.heldFor(node, merged);
|
|
2646
3196
|
this.movePaint(node, merged);
|
|
2647
3197
|
return merged;
|
|
2648
3198
|
}
|
|
@@ -2657,9 +3207,7 @@ export class Engine {
|
|
|
2657
3207
|
const from = node.parent?.paintsOn === node ? node.parent : null;
|
|
2658
3208
|
if (!from)
|
|
2659
3209
|
return;
|
|
2660
|
-
const paint =
|
|
2661
|
-
...this.styles.resolve(from, this.styleEpoch).style,
|
|
2662
|
-
});
|
|
3210
|
+
const paint = { ...this.styles.resolve(from, this.styleEpoch).style, ...inlineOf(from) };
|
|
2663
3211
|
for (const key of PAINT_KEYS)
|
|
2664
3212
|
if (paint[key] !== undefined)
|
|
2665
3213
|
merged[key] = paint[key];
|
|
@@ -2718,7 +3266,7 @@ export class Engine {
|
|
|
2718
3266
|
if (!this.inlineDirection)
|
|
2719
3267
|
return own;
|
|
2720
3268
|
for (let at = node; at; at = at.parent) {
|
|
2721
|
-
const inline = textDirection(
|
|
3269
|
+
const inline = textDirection(inlineOf(at)['direction']);
|
|
2722
3270
|
if (inline)
|
|
2723
3271
|
return inline;
|
|
2724
3272
|
const cascaded = textDirection(at.styleCache?.style['direction']);
|
|
@@ -2746,6 +3294,7 @@ export class Engine {
|
|
|
2746
3294
|
if (!sheet || this.knownSheets.has(sheet))
|
|
2747
3295
|
return;
|
|
2748
3296
|
this.knownSheets.add(sheet);
|
|
3297
|
+
this.styles.noteSheet(sheet);
|
|
2749
3298
|
this.sheetOrder.push(sheet);
|
|
2750
3299
|
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
2751
3300
|
this.keyframes.set(name, sheet.keyframes[name]);
|
|
@@ -2773,6 +3322,7 @@ export class Engine {
|
|
|
2773
3322
|
this.knownSheets.delete(sheet);
|
|
2774
3323
|
if (next && !this.knownSheets.has(next)) {
|
|
2775
3324
|
this.knownSheets.add(next);
|
|
3325
|
+
this.styles.noteSheet(next);
|
|
2776
3326
|
this.sheetOrder[at] = next;
|
|
2777
3327
|
}
|
|
2778
3328
|
else {
|
|
@@ -2919,11 +3469,69 @@ export class Engine {
|
|
|
2919
3469
|
* runs it frame by frame, and JavaScript hears of it again when it ends. Anything else is
|
|
2920
3470
|
* played from here, a commit a frame. See `NativeScrollDriver.play`.
|
|
2921
3471
|
*/
|
|
3472
|
+
/**
|
|
3473
|
+
* The nodes whose animation moves them by a share of their own size and had no size yet when
|
|
3474
|
+
* it started, each with the style it rests at: asked again once the commit has laid them out.
|
|
3475
|
+
*/
|
|
3476
|
+
unsized = new Map();
|
|
3477
|
+
/**
|
|
3478
|
+
* An animation's frames as native can interpolate them: a move by a share of the box,
|
|
3479
|
+
* `translateX(200%)`, as the points that is of the size the view is laid out at. Nothing where
|
|
3480
|
+
* the view has no size yet, which is every view until its first commit: it is played from
|
|
3481
|
+
* JavaScript, and handed over once that commit has laid it out. See `playSized`.
|
|
3482
|
+
*
|
|
3483
|
+
* ponytail: the size it had when it started. A box that changes size goes on moving by the
|
|
3484
|
+
* points of the old one until the animation starts again; restart it from the box's layout
|
|
3485
|
+
* event if a bar that resizes mid-animation comes up.
|
|
3486
|
+
*/
|
|
3487
|
+
sized(node, running, props) {
|
|
3488
|
+
// One that waits, or takes no time, is played from here whatever it moves by.
|
|
3489
|
+
if (running.spec.delay !== 0 || running.spec.duration <= 0)
|
|
3490
|
+
return null;
|
|
3491
|
+
if (!movesByShare(running.tracks, props))
|
|
3492
|
+
return { tracks: running.tracks, resting: props };
|
|
3493
|
+
let size;
|
|
3494
|
+
this.measure(node, (frame) => (size = frame));
|
|
3495
|
+
if (size)
|
|
3496
|
+
return sharesAsPoints(running.tracks, props, size);
|
|
3497
|
+
this.unsized.set(node, props);
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
3500
|
+
/**
|
|
3501
|
+
* Hand to native each animation that was waiting for its view to have a size. The commit just
|
|
3502
|
+
* made painted its first frame, so its clock starts here: no frame of it has been seen move.
|
|
3503
|
+
*/
|
|
3504
|
+
playSized() {
|
|
3505
|
+
if (!this.unsized.size)
|
|
3506
|
+
return;
|
|
3507
|
+
const waiting = [...this.unsized];
|
|
3508
|
+
this.unsized.clear();
|
|
3509
|
+
for (const [node, props] of waiting) {
|
|
3510
|
+
const running = node.playing;
|
|
3511
|
+
if (!running || running.done || running.native || !this.playing.has(node))
|
|
3512
|
+
continue;
|
|
3513
|
+
// Native plays it from its first frame, so its clock starts here: but only where native
|
|
3514
|
+
// takes it. Left in JavaScript, it goes on from the clock it started on.
|
|
3515
|
+
const started = running.start;
|
|
3516
|
+
running.start = this.now();
|
|
3517
|
+
if (this.playNatively(node, running, props)) {
|
|
3518
|
+
this.playing.delete(node);
|
|
3519
|
+
// `collapsable`, which keeps a view for native to move, is in what it holds now.
|
|
3520
|
+
this.markProps(node, false);
|
|
3521
|
+
}
|
|
3522
|
+
else {
|
|
3523
|
+
running.start = started;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
// One with no size after its commit is not laid out at all, and is played from JavaScript.
|
|
3527
|
+
this.unsized.clear();
|
|
3528
|
+
}
|
|
2922
3529
|
playNatively(node, running, props) {
|
|
2923
3530
|
const { spec } = running;
|
|
2924
|
-
|
|
3531
|
+
const sized = this.scrollDriver ? this.sized(node, running, props) : null;
|
|
3532
|
+
if (!this.scrollDriver || !sized)
|
|
2925
3533
|
return false;
|
|
2926
|
-
const { channels, held, span } = clockChannels(
|
|
3534
|
+
const { channels, held, span } = clockChannels(sized.tracks, spec, sized.resting);
|
|
2927
3535
|
if (held.length || !(channels.opacity || channels.transform.length))
|
|
2928
3536
|
return false;
|
|
2929
3537
|
// There and back is one native animation: an odd number of ways has no whole number of them.
|
|
@@ -3044,6 +3652,10 @@ export class Engine {
|
|
|
3044
3652
|
reframed.values = finished && !holds ? {} : values;
|
|
3045
3653
|
this.playedFrames.set(reframed, frames);
|
|
3046
3654
|
node.playing = reframed;
|
|
3655
|
+
// One that had nothing to paint was let go of: with frames that say something it plays on.
|
|
3656
|
+
const stopped = reframed.done || reframed.pausedAt !== undefined || reframed.native;
|
|
3657
|
+
if (!stopped)
|
|
3658
|
+
this.playing.add(node);
|
|
3047
3659
|
}
|
|
3048
3660
|
/**
|
|
3049
3661
|
* `animation-play-state` changed on an animation already playing: pausing holds the frame it
|
|
@@ -3208,12 +3820,7 @@ export class Engine {
|
|
|
3208
3820
|
return props;
|
|
3209
3821
|
const state = (node.transitions ??= new Map());
|
|
3210
3822
|
const now = this.now();
|
|
3211
|
-
|
|
3212
|
-
// transitioned once it has been seen settling, and one that does not exist is never seen - so
|
|
3213
|
-
// `transition: transform` with no transform until a class adds one would arrive instantly,
|
|
3214
|
-
// which is the commonest way to write one. `all` names nothing, so it keeps the old rule.
|
|
3215
|
-
const named = spec ? Object.keys(spec).filter((key) => key !== 'all') : [];
|
|
3216
|
-
for (const key of new Set([...Object.keys(props), ...named])) {
|
|
3823
|
+
for (const key of steppedKeys(props, spec, state)) {
|
|
3217
3824
|
if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
|
|
3218
3825
|
this.running.add(node);
|
|
3219
3826
|
this.emitTransition(node, 'topTransitionstart', key);
|
|
@@ -3223,6 +3830,21 @@ export class Engine {
|
|
|
3223
3830
|
node.transitions = undefined;
|
|
3224
3831
|
return props;
|
|
3225
3832
|
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Whether an animation that has finished goes on holding its last frame. With a forwards fill
|
|
3835
|
+
* it does, for good: without one the element falls back to whatever the cascade gives it, and
|
|
3836
|
+
* 'backwards' fills the start only. Heard, it holds until whoever listens has been told: see
|
|
3837
|
+
* `lettingGo`.
|
|
3838
|
+
*/
|
|
3839
|
+
keeps(node, running) {
|
|
3840
|
+
if (running.spec.fill === 'forwards' || running.spec.fill === 'both')
|
|
3841
|
+
return true;
|
|
3842
|
+
// One that listened and has stopped leaves an empty set behind: nobody to hear it.
|
|
3843
|
+
if (!node.listeners?.get('topAnimationend')?.size)
|
|
3844
|
+
return false;
|
|
3845
|
+
this.lettingGo.add(node);
|
|
3846
|
+
return true;
|
|
3847
|
+
}
|
|
3226
3848
|
/** One `@keyframes` player, one frame on. */
|
|
3227
3849
|
advancePlayer(node, now) {
|
|
3228
3850
|
const running = node.playing;
|
|
@@ -3234,12 +3856,19 @@ export class Engine {
|
|
|
3234
3856
|
this.playing.delete(node);
|
|
3235
3857
|
return;
|
|
3236
3858
|
}
|
|
3859
|
+
// Frames that say nothing paint nothing: a keyframe's one declaration refused at build time
|
|
3860
|
+
// leaves these. No frame of it is committed, and one that never ends has nothing left to do.
|
|
3861
|
+
const idle = running.tracks.size === 0;
|
|
3237
3862
|
const { values, finished } = sample(running, now);
|
|
3238
|
-
//
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3863
|
+
// One of no time is finished already, endless or not, and ends as any other does.
|
|
3864
|
+
if (idle && !finished && running.spec.iterations === null) {
|
|
3865
|
+
this.playing.delete(node);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
running.values = finished && !this.keeps(node, running) ? {} : values;
|
|
3869
|
+
// Its end is still a commit: that is what its `animationend` is sent after.
|
|
3870
|
+
if (!idle || finished)
|
|
3871
|
+
this.markProps(node, false);
|
|
3243
3872
|
if (!finished)
|
|
3244
3873
|
return;
|
|
3245
3874
|
running.done = true;
|
|
@@ -3360,6 +3989,58 @@ export class Engine {
|
|
|
3360
3989
|
};
|
|
3361
3990
|
});
|
|
3362
3991
|
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Mark the text field each box of a row aligned by baseline takes its baseline from, before
|
|
3994
|
+
* the field is merged, and unmark it when the row is aligned another way.
|
|
3995
|
+
*/
|
|
3996
|
+
noteBaselineFields(row) {
|
|
3997
|
+
const aligned = ownLayout(row, 'alignItems') === 'baseline';
|
|
3998
|
+
const before = row.baselineFields;
|
|
3999
|
+
if (!aligned && !before)
|
|
4000
|
+
return;
|
|
4001
|
+
const now = new Set(aligned ? row.children.filter(inFlow).map(fieldOnBaseline).filter(isNode) : []);
|
|
4002
|
+
// One that is no longer this row's, and that no other row has marked since.
|
|
4003
|
+
for (const field of before ?? []) {
|
|
4004
|
+
if (!now.has(field) && field.onBaseline === row)
|
|
4005
|
+
this.onBaselineOf(field, undefined);
|
|
4006
|
+
}
|
|
4007
|
+
for (const field of now)
|
|
4008
|
+
if (field.onBaseline !== row)
|
|
4009
|
+
this.onBaselineOf(field, row);
|
|
4010
|
+
row.baselineFields = now.size ? now : undefined;
|
|
4011
|
+
}
|
|
4012
|
+
onBaselineOf(field, row) {
|
|
4013
|
+
field.onBaseline = row;
|
|
4014
|
+
this.markProps(field, false);
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Have the text a row aligned by baseline takes a baseline from measured again, where that
|
|
4018
|
+
* text is inside one of the row's boxes and the row is committed again. React Native keeps
|
|
4019
|
+
* what a paragraph measured on the one copy of its node that was measured, and Yoga copies a
|
|
4020
|
+
* node it lays out around: a row that is laid out again asks a box for its baseline, the box
|
|
4021
|
+
* asks the paragraph in it, and a copy that was never measured works it out then, on a node
|
|
4022
|
+
* that is by then not to be changed. A debug build stops there. Committed to be measured
|
|
4023
|
+
* again (`remeasure`), the paragraph is measured in this commit and has the answer.
|
|
4024
|
+
*
|
|
4025
|
+
* ponytail: the first box in the flow at each level, which is the one Yoga asks unless
|
|
4026
|
+
* another is aligned by baseline itself. A row a commit lays out again without the engine
|
|
4027
|
+
* cloning it, as its room changing does, is not reached: clone on a layout event if one is
|
|
4028
|
+
* seen to stop there.
|
|
4029
|
+
*/
|
|
4030
|
+
freshBaselines(row) {
|
|
4031
|
+
if (!row.committed || ownLayout(row, 'alignItems') !== 'baseline')
|
|
4032
|
+
return;
|
|
4033
|
+
for (const item of row.children) {
|
|
4034
|
+
let at = firstInFlow(item);
|
|
4035
|
+
while (at?.committed && committedViewName(at) !== PARAGRAPH)
|
|
4036
|
+
at = firstInFlow(at);
|
|
4037
|
+
if (!at?.committed)
|
|
4038
|
+
continue;
|
|
4039
|
+
at.remeasure = true;
|
|
4040
|
+
for (let up = at; up && up !== row; up = up.parent)
|
|
4041
|
+
up.subtreeDirty = true;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
3363
4044
|
/**
|
|
3364
4045
|
* Copy a list of gradients, running every stop's colour through the host's converter.
|
|
3365
4046
|
*
|
|
@@ -3403,6 +4084,8 @@ export class Engine {
|
|
|
3403
4084
|
this.refitChildren(node);
|
|
3404
4085
|
const viewName = committedViewName(node);
|
|
3405
4086
|
this.forgetRenamed(node, viewName);
|
|
4087
|
+
this.noteBaselineFields(node);
|
|
4088
|
+
this.freshBaselines(node);
|
|
3406
4089
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
3407
4090
|
noteTouches(node, style);
|
|
3408
4091
|
const previous = node.committed;
|
|
@@ -3539,6 +4222,8 @@ export class Engine {
|
|
|
3539
4222
|
* when it is shown again, as Modal.js creates it afresh. Committed again, it ignores every touch.
|
|
3540
4223
|
*/
|
|
3541
4224
|
withheld(child) {
|
|
4225
|
+
if (this.undisplayed(child))
|
|
4226
|
+
return true;
|
|
3542
4227
|
if (child.props['visible'] !== false || child.presented)
|
|
3543
4228
|
return false;
|
|
3544
4229
|
if (viewNameOf(child) !== MODAL_HOST)
|
|
@@ -3547,6 +4232,42 @@ export class Engine {
|
|
|
3547
4232
|
this.forgetCommitted(child);
|
|
3548
4233
|
return true;
|
|
3549
4234
|
}
|
|
4235
|
+
/**
|
|
4236
|
+
* Whether an element is `display: none`, which has no box and so no view, nor has anything
|
|
4237
|
+
* in it. A view that is committed and not displayed looks the same, but Yoga marks one as
|
|
4238
|
+
* laid out each time it measures what holds it, and React Native clears the mark only where it
|
|
4239
|
+
* reads that parent's layout: a debug build stops at the mark, on a later commit that has the
|
|
4240
|
+
* parent's layout already. Shown again it is made again, as an element put back in the tree
|
|
4241
|
+
* is: what its views held of their own, a scroll offset, is not kept.
|
|
4242
|
+
*/
|
|
4243
|
+
undisplayed(child) {
|
|
4244
|
+
if (child.kind !== 'element')
|
|
4245
|
+
return false;
|
|
4246
|
+
this.styles.resolve(child, this.styleEpoch);
|
|
4247
|
+
if (ownLayout(child, 'display') !== 'none')
|
|
4248
|
+
return false;
|
|
4249
|
+
if (child.committed) {
|
|
4250
|
+
this.forgetCommitted(child);
|
|
4251
|
+
// A field in it has no view to send its blur from.
|
|
4252
|
+
if (isWithin(this.focusedNode, child))
|
|
4253
|
+
this.setFocused(null);
|
|
4254
|
+
}
|
|
4255
|
+
return true;
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Whether a node hoisted into `target` is not displayed where it was written: it, or a box
|
|
4259
|
+
* between it and the child of `target` it was written in. That child is asked before this is.
|
|
4260
|
+
*/
|
|
4261
|
+
undisplayedUnder(target, written) {
|
|
4262
|
+
for (let at = written; at && at.parent !== target; at = at.parent) {
|
|
4263
|
+
if (!this.undisplayed(at))
|
|
4264
|
+
continue;
|
|
4265
|
+
if (written.committed)
|
|
4266
|
+
this.forgetCommitted(written);
|
|
4267
|
+
return true;
|
|
4268
|
+
}
|
|
4269
|
+
return false;
|
|
4270
|
+
}
|
|
3550
4271
|
/**
|
|
3551
4272
|
* `reconcileChildren` for a node other nodes hoist into: its own children, each followed by
|
|
3552
4273
|
* the hoisted nodes written inside it, then those written in it directly, then the views it
|
|
@@ -3569,9 +4290,10 @@ export class Engine {
|
|
|
3569
4290
|
else
|
|
3570
4291
|
handles.push(this.reconcileUnder(node, child, context));
|
|
3571
4292
|
const moved = hoisted?.get(child);
|
|
3572
|
-
|
|
3573
|
-
|
|
4293
|
+
for (const written of moved ?? []) {
|
|
4294
|
+
if (!this.undisplayedUnder(node, written))
|
|
3574
4295
|
handles.push(this.land(node, written, landed));
|
|
4296
|
+
}
|
|
3575
4297
|
}
|
|
3576
4298
|
for (const child of direct)
|
|
3577
4299
|
handles.push(this.land(node, child, landed));
|
|
@@ -3860,9 +4582,12 @@ export class Engine {
|
|
|
3860
4582
|
this.registerEventHandler();
|
|
3861
4583
|
this.responders.set(node, handlers);
|
|
3862
4584
|
this.keepNative(node, true);
|
|
4585
|
+
// What it is committed with can hang on whether it takes a touch: see `stillTouched`.
|
|
4586
|
+
this.markProps(node);
|
|
3863
4587
|
return () => {
|
|
3864
4588
|
this.responders.delete(node);
|
|
3865
4589
|
this.keepNative(node, false);
|
|
4590
|
+
this.markProps(node);
|
|
3866
4591
|
if (this.currentResponder !== node)
|
|
3867
4592
|
return;
|
|
3868
4593
|
// Torn down under the finger: no release is coming, so the chain it marked is cleared
|
|
@@ -4094,7 +4819,9 @@ export class Engine {
|
|
|
4094
4819
|
const handle = node.committed?.handle;
|
|
4095
4820
|
if (!handle || !this.fabric.setIsJSResponder)
|
|
4096
4821
|
return;
|
|
4097
|
-
const
|
|
4822
|
+
const asked = this.responders.get(node)?.blockNativeResponder ?? false;
|
|
4823
|
+
// And for a drag an element's `touch-action` keeps from the page: see `holdSwipe`.
|
|
4824
|
+
const block = asked || this.drag?.held === true;
|
|
4098
4825
|
this.fabric.setIsJSResponder(handle, isResponder, block);
|
|
4099
4826
|
}
|
|
4100
4827
|
/** Runs before listener propagation, as React's responder plugin does. */
|
|
@@ -4156,6 +4883,10 @@ export class Engine {
|
|
|
4156
4883
|
dispatchEvent(instanceHandle, topLevelType, nativeEvent) {
|
|
4157
4884
|
const target = instanceHandle;
|
|
4158
4885
|
this.trace(target, topLevelType);
|
|
4886
|
+
// What has the focus does not take it again: native answering a focus it was told of, where
|
|
4887
|
+
// whoever asked has already said so.
|
|
4888
|
+
if (topLevelType === 'topFocus' && target !== null && this.focusedNode === target)
|
|
4889
|
+
return;
|
|
4159
4890
|
// Listeners get RN's documented shape, `{nativeEvent}`. Fabric hands us the payload bare;
|
|
4160
4891
|
// React wraps it in a synthetic event and every RN API is written against `event.nativeEvent`,
|
|
4161
4892
|
// so handlers ported from RN would silently read undefined otherwise.
|
|
@@ -4166,6 +4897,7 @@ export class Engine {
|
|
|
4166
4897
|
if (topLevelType === 'topScroll' && target && this.scrollTimelines.has(target)) {
|
|
4167
4898
|
this.measuredScroll(target, nativeEvent);
|
|
4168
4899
|
}
|
|
4900
|
+
this.holdSwipe(target, topLevelType, nativeEvent);
|
|
4169
4901
|
this.trackFocus(target, topLevelType);
|
|
4170
4902
|
if (target)
|
|
4171
4903
|
this.cancelPressForScroll(target, topLevelType, event);
|
|
@@ -4177,6 +4909,116 @@ export class Engine {
|
|
|
4177
4909
|
}
|
|
4178
4910
|
this.propagate(target, topLevelType, event);
|
|
4179
4911
|
}
|
|
4912
|
+
/** The screen whose swipe back is held off while a finger is on what keeps a sideways drag. */
|
|
4913
|
+
swipeHeld = null;
|
|
4914
|
+
/**
|
|
4915
|
+
* Hold a screen's swipe back off for a touch that starts on an element whose `touch-action`
|
|
4916
|
+
* keeps a drag to the side for itself, and give it back when the last finger lifts. From iOS
|
|
4917
|
+
* 26 the swipe starts anywhere on a screen once a finger has moved a little to the right, and
|
|
4918
|
+
* takes the touch from whatever was following it: a slider's thumb stops and the screen
|
|
4919
|
+
* leaves. The screen is told before the finger has moved that far.
|
|
4920
|
+
*/
|
|
4921
|
+
holdSwipe(target, type, nativeEvent) {
|
|
4922
|
+
if (type === TOUCH_START)
|
|
4923
|
+
this.holdFrom(target, nativeEvent);
|
|
4924
|
+
else if (type === 'topTouchMove')
|
|
4925
|
+
this.settleDrag(nativeEvent);
|
|
4926
|
+
else if (type === 'topTouchEnd' || type === 'topTouchCancel')
|
|
4927
|
+
this.releaseSwipe(nativeEvent);
|
|
4928
|
+
}
|
|
4929
|
+
/**
|
|
4930
|
+
* A touch on an element that keeps a drag to the side: where it began, and whether native's
|
|
4931
|
+
* own gestures are held off for it. Held from the start where the element keeps a drag down
|
|
4932
|
+
* the page too (`none`); from the moment the finger sets off across where it leaves that one
|
|
4933
|
+
* to the page (`pan-y`), and never for that touch where it sets off down.
|
|
4934
|
+
*/
|
|
4935
|
+
drag = null;
|
|
4936
|
+
holdFrom(target, nativeEvent) {
|
|
4937
|
+
// A first finger down is a new touch: the last one's end never came where what it was on
|
|
4938
|
+
// went from under the finger.
|
|
4939
|
+
const fingers = nativeEvent?.touches?.length;
|
|
4940
|
+
if (this.drag && fingers === 1)
|
|
4941
|
+
this.releaseSwipe(null);
|
|
4942
|
+
if (this.drag || !target)
|
|
4943
|
+
return;
|
|
4944
|
+
const actions = touchActions(target);
|
|
4945
|
+
if (!keeps(actions, PANS_ACROSS))
|
|
4946
|
+
return;
|
|
4947
|
+
const all = keeps(actions, PANS_ALONG);
|
|
4948
|
+
this.drag = { ...pointOf(nativeEvent), settled: all, held: all, on: target };
|
|
4949
|
+
if (all)
|
|
4950
|
+
this.holdScroll(target);
|
|
4951
|
+
const screen = this.screenOf(target);
|
|
4952
|
+
if (!screen)
|
|
4953
|
+
return;
|
|
4954
|
+
this.swipeHeld = screen;
|
|
4955
|
+
this.tellScreen(screen);
|
|
4956
|
+
}
|
|
4957
|
+
/** The scroll view whose scrolling is off while a drag over it is an element's own. */
|
|
4958
|
+
scrollHeld = null;
|
|
4959
|
+
/**
|
|
4960
|
+
* Stop the scroll view a node is in from scrolling, until the touch ends. Telling native who
|
|
4961
|
+
* holds the touch does this on Android. On iOS a scroll view is held off only by a responder
|
|
4962
|
+
* that is over it, never one inside it, so it is told not to scroll.
|
|
4963
|
+
*/
|
|
4964
|
+
holdScroll(node) {
|
|
4965
|
+
for (let up = node.parent; up; up = up.parent) {
|
|
4966
|
+
if (up.kind !== 'element' || !SCROLL_VIEWS.has(viewNameOf(up)))
|
|
4967
|
+
continue;
|
|
4968
|
+
this.scrollHeld = up;
|
|
4969
|
+
this.tellScreen(up);
|
|
4970
|
+
return;
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
/** Settle which way a drag set off, and hold native's own off one that set off across. */
|
|
4974
|
+
settleDrag(nativeEvent) {
|
|
4975
|
+
const drag = this.drag;
|
|
4976
|
+
if (!drag || drag.settled)
|
|
4977
|
+
return;
|
|
4978
|
+
const at = pointOf(nativeEvent);
|
|
4979
|
+
const [across, along] = [Math.abs(at.x - drag.x), Math.abs(at.y - drag.y)];
|
|
4980
|
+
if (Math.max(across, along) < DRAG_SLOP)
|
|
4981
|
+
return;
|
|
4982
|
+
drag.settled = true;
|
|
4983
|
+
drag.held = across > along;
|
|
4984
|
+
if (!drag.held)
|
|
4985
|
+
return;
|
|
4986
|
+
if (this.currentResponder)
|
|
4987
|
+
this.tellNative(this.currentResponder, true);
|
|
4988
|
+
this.holdScroll(drag.on);
|
|
4989
|
+
}
|
|
4990
|
+
/** Give the swipe back once the last finger has lifted. */
|
|
4991
|
+
releaseSwipe(nativeEvent) {
|
|
4992
|
+
const left = nativeEvent?.touches?.length ?? 0;
|
|
4993
|
+
if (left > 0)
|
|
4994
|
+
return;
|
|
4995
|
+
this.drag = null;
|
|
4996
|
+
for (const held of [this.scrollHeld, this.swipeHeld]) {
|
|
4997
|
+
if (held === this.scrollHeld)
|
|
4998
|
+
this.scrollHeld = null;
|
|
4999
|
+
else
|
|
5000
|
+
this.swipeHeld = null;
|
|
5001
|
+
if (held)
|
|
5002
|
+
this.tellScreen(held);
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
/**
|
|
5006
|
+
* Commit a screen or a scroll view again now, not with whatever next changes: the swipe begins
|
|
5007
|
+
* once the finger has moved ten points or so, which is a frame or two after it came down.
|
|
5008
|
+
*/
|
|
5009
|
+
tellScreen(screen) {
|
|
5010
|
+
// Its props alone: nothing a selector reads moved, so nothing under it is styled again.
|
|
5011
|
+
this.markProps(screen, false);
|
|
5012
|
+
this.commit();
|
|
5013
|
+
}
|
|
5014
|
+
/** The screen of a native stack a node is on, or nothing where it is on none. */
|
|
5015
|
+
screenOf(node) {
|
|
5016
|
+
for (let up = node; up; up = up.parent) {
|
|
5017
|
+
if (up.kind === 'element' && viewNameOf(up) === STACK_SCREEN)
|
|
5018
|
+
return up;
|
|
5019
|
+
}
|
|
5020
|
+
return null;
|
|
5021
|
+
}
|
|
4180
5022
|
/** A presented modal host has finished leaving, so a hidden one can come out of the tree. */
|
|
4181
5023
|
dismissed(target) {
|
|
4182
5024
|
if (!target.presented)
|