@ng-native/fabric 0.4.0 → 0.6.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/color-mix.js +1 -16
- package/dist/color-mix.js.map +1 -1
- package/dist/css.d.ts +299 -28
- package/dist/css.js +1093 -122
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +267 -3
- package/dist/engine.js +1209 -76
- package/dist/engine.js.map +1 -1
- package/dist/inline-token.js +27 -15
- 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.d.ts +21 -0
- package/dist/native-drive.js +65 -0
- package/dist/native-drive.js.map +1 -1
- package/dist/premultiplied-stops.d.ts +7 -0
- package/dist/premultiplied-stops.js +131 -0
- package/dist/premultiplied-stops.js.map +1 -0
- package/dist/scroll-animation.d.ts +24 -0
- package/dist/scroll-animation.js +82 -4
- package/dist/scroll-animation.js.map +1 -1
- package/dist/transition.d.ts +15 -0
- package/dist/transition.js +134 -11
- package/dist/transition.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* because a persistent parent holds references to specific child handles. That bubble is
|
|
8
8
|
* inherent and is what React's own Fabric renderer does.
|
|
9
9
|
*/
|
|
10
|
-
import { boundDeclaration, inlineInherited, setsInherited, StyleResolver, } 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
13
|
import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
|
|
@@ -15,8 +15,9 @@ 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 { 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
|
+
import { premultipliedStops } from "./premultiplied-stops.js";
|
|
20
21
|
/**
|
|
21
22
|
* The animation a node's style asks for, with a play state from a rule of its own applied. Both
|
|
22
23
|
* are instructions for this engine and nothing native has heard of them, so they come out of the
|
|
@@ -400,6 +401,38 @@ function textScale(props, fontScale = 1) {
|
|
|
400
401
|
const cap = props['maxFontSizeMultiplier'];
|
|
401
402
|
return typeof cap === 'number' && cap >= 1 ? Math.min(fontScale, cap) : fontScale;
|
|
402
403
|
}
|
|
404
|
+
/** A field's padding and border on its top or bottom edge, as they are written. */
|
|
405
|
+
const blockEdge = (props, side, logical) => [
|
|
406
|
+
props[`padding${side}`] ??
|
|
407
|
+
props[`paddingBlock${logical}`] ??
|
|
408
|
+
props['paddingBlock'] ??
|
|
409
|
+
props['paddingVertical'] ??
|
|
410
|
+
props['padding'] ??
|
|
411
|
+
0,
|
|
412
|
+
props[`border${side}Width`] ?? props['borderWidth'] ?? 0,
|
|
413
|
+
];
|
|
414
|
+
/**
|
|
415
|
+
* A multiline field's `rows` as its height, where it was given none: that many lines of its
|
|
416
|
+
* line height at the system text size, inside its padding and border, as a browser sizes a `<textarea>`. A native field
|
|
417
|
+
* is as tall as its text, which is one line before anything is typed.
|
|
418
|
+
*/
|
|
419
|
+
function rowsTall(props, fontScale) {
|
|
420
|
+
const rows = Number(props['rows']);
|
|
421
|
+
const line = props['lineHeight'];
|
|
422
|
+
if (props['multiline'] !== true || !(rows > 0) || typeof line !== 'number')
|
|
423
|
+
return;
|
|
424
|
+
if (isSet(props['height']) && props['height'] !== 'auto')
|
|
425
|
+
return;
|
|
426
|
+
// A content box's height is its lines alone: Yoga adds the padding and border itself.
|
|
427
|
+
const edges = props['boxSizing'] === 'content-box'
|
|
428
|
+
? []
|
|
429
|
+
: [...blockEdge(props, 'Top', 'Start'), ...blockEdge(props, 'Bottom', 'End')];
|
|
430
|
+
if (!edges.every((part) => typeof part === 'number'))
|
|
431
|
+
return;
|
|
432
|
+
// At the system text size, which the field's lines are drawn at.
|
|
433
|
+
const lines = rows * line * textScale(props, fontScale);
|
|
434
|
+
props['height'] = edges.reduce((sum, part) => sum + part, lines);
|
|
435
|
+
}
|
|
403
436
|
/**
|
|
404
437
|
* Centre the text of a single-line text field that has a line height, as Chrome centres an
|
|
405
438
|
* input's, keeping the height the line height gives it.
|
|
@@ -437,15 +470,7 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
437
470
|
// Android sizes the field by its line height, and centres the text in it, itself.
|
|
438
471
|
if (viewName !== 'TextInput')
|
|
439
472
|
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
|
-
];
|
|
473
|
+
const edge = (side, logical) => blockEdge(props, side, logical);
|
|
449
474
|
const line = lineHeight * textScale(props, fontScale);
|
|
450
475
|
const box = [line, ...edge('Top', 'Start'), ...edge('Bottom', 'End')];
|
|
451
476
|
const own = props['minHeight'] ?? 0;
|
|
@@ -459,6 +484,255 @@ function centreSingleLine(viewName, props, fontScale) {
|
|
|
459
484
|
: box.reduce((sum, part) => sum + part, 0);
|
|
460
485
|
props['minHeight'] = Math.max(own, Math.min(content, max));
|
|
461
486
|
}
|
|
487
|
+
const NO_STYLE = {};
|
|
488
|
+
/**
|
|
489
|
+
* An element's inline style, less each property a rule it matches declared `!important`: an
|
|
490
|
+
* inline style is the last of the plain declarations, under every important one.
|
|
491
|
+
*
|
|
492
|
+
* ponytail: by the name a property is committed under, so an inline `padding` is not taken out
|
|
493
|
+
* by an important `padding-top`. Expand the inline shorthand here if one is ever written so.
|
|
494
|
+
*/
|
|
495
|
+
function inlineOf(node) {
|
|
496
|
+
if (!node.props['style'])
|
|
497
|
+
return NO_STYLE;
|
|
498
|
+
const inline = flattenStyle(node.props['style'], {});
|
|
499
|
+
for (const key of node.styleCache?.important ?? [])
|
|
500
|
+
delete inline[key];
|
|
501
|
+
return inline;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* What a container's own style says for `key`, in the order `mergeProps` applies them: a
|
|
505
|
+
* component's override, inline, a prop, its stylesheet, its default.
|
|
506
|
+
*/
|
|
507
|
+
function ownLayout(node, key) {
|
|
508
|
+
return (flattenStyle(node.props[STYLE_OVERRIDE], {})[key] ??
|
|
509
|
+
inlineOf(node)[key] ??
|
|
510
|
+
node.props[key] ??
|
|
511
|
+
node.styleCache?.style[key] ??
|
|
512
|
+
node.defaultStyle?.[key]);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* A container's main axis and how it aligns across it, as `row` or `column` and a keyword. A
|
|
516
|
+
* `display: contents` node is no container: its children are laid out in the one above it.
|
|
517
|
+
*/
|
|
518
|
+
function containerOf(node) {
|
|
519
|
+
if (ownLayout(node, 'display') === 'contents')
|
|
520
|
+
return 'contents';
|
|
521
|
+
const direction = String(ownLayout(node, 'flexDirection') ?? 'column');
|
|
522
|
+
return `${direction.startsWith('row') ? 'row' : 'column'} ${ownLayout(node, 'alignItems') ?? 'stretch'}`;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* The node a box is laid out in: its parent, or the one above every `display: contents` ancestor.
|
|
526
|
+
* Each of those is told a `fit-content` child passed through, so that when it stops being
|
|
527
|
+
* `contents`, and is the container, the child is worked out again.
|
|
528
|
+
*/
|
|
529
|
+
function layoutParent(node) {
|
|
530
|
+
let parent = node.parent;
|
|
531
|
+
while (parent && containerOf(parent) === 'contents') {
|
|
532
|
+
parent.fitContainer = 'contents';
|
|
533
|
+
parent = parent.parent;
|
|
534
|
+
}
|
|
535
|
+
return parent;
|
|
536
|
+
}
|
|
537
|
+
const STRETCHES = new Set([undefined, null, 'auto', 'stretch']);
|
|
538
|
+
/**
|
|
539
|
+
* `width: fit-content` and `height: fit-content`, which Yoga has no size for.
|
|
540
|
+
*
|
|
541
|
+
* Along its container's main axis a box is as big as its content already, so the size is
|
|
542
|
+
* dropped. Across it a box stretches, where a browser keeps it at its content's size and at the
|
|
543
|
+
* start: `align-self: flex-start` does both. An alignment the container or the box asked for is
|
|
544
|
+
* kept, since it stops the stretch already.
|
|
545
|
+
*/
|
|
546
|
+
function fitContent(node, props) {
|
|
547
|
+
const width = props['width'] === 'fit-content';
|
|
548
|
+
const height = props['height'] === 'fit-content';
|
|
549
|
+
if (!width && !height)
|
|
550
|
+
return;
|
|
551
|
+
if (width)
|
|
552
|
+
delete props['width'];
|
|
553
|
+
if (height)
|
|
554
|
+
delete props['height'];
|
|
555
|
+
const parent = layoutParent(node);
|
|
556
|
+
if (!parent)
|
|
557
|
+
return;
|
|
558
|
+
const container = containerOf(parent);
|
|
559
|
+
parent.fitContainer = container;
|
|
560
|
+
const [direction, align] = container.split(' ');
|
|
561
|
+
const across = direction === 'row' ? height : width;
|
|
562
|
+
if (across && align === 'stretch' && STRETCHES.has(props['alignSelf'])) {
|
|
563
|
+
props['alignSelf'] = 'flex-start';
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const isPercent = (value) => typeof value === 'string' && value.endsWith('%');
|
|
567
|
+
/** Whether a `flex-basis` is a length or a percentage of something: not zero, and not `auto`. */
|
|
568
|
+
const isSizedBasis = (basis) => isPercent(basis)
|
|
569
|
+
? Number.parseFloat(basis) !== 0
|
|
570
|
+
: typeof basis === 'number' && basis !== 0;
|
|
571
|
+
/**
|
|
572
|
+
* A `flex-basis` that is a length or a percentage, as the size it is along its container's main
|
|
573
|
+
* axis: a width in a row and a height in a column, over one written for that axis.
|
|
574
|
+
*
|
|
575
|
+
* Yoga works a `flexBasis` out once for a view and keeps the answer, and React Native never has
|
|
576
|
+
* it forgotten when the view's props change (`computedFlexBasis`, cleared only by
|
|
577
|
+
* `markDirtyAndPropagate`). A basis that changes after the first layout is not read again, and
|
|
578
|
+
* a percentage does not follow the box it is a share of. A size is worked out on every pass, and
|
|
579
|
+
* with no basis Yoga takes the size along the main axis as the basis: the same sum.
|
|
580
|
+
*
|
|
581
|
+
* A basis of zero is left as it is, which is every `flex: 1`: under a container with no size on
|
|
582
|
+
* its main axis Yoga passes over a basis and sizes the box by its content, where a size of zero
|
|
583
|
+
* would be zero. A box out of the flow is no flex item, and keeps its own size.
|
|
584
|
+
*/
|
|
585
|
+
function basisAsSize(node, props) {
|
|
586
|
+
const basis = props['flexBasis'];
|
|
587
|
+
if (!isSizedBasis(basis) || props['position'] === 'absolute')
|
|
588
|
+
return;
|
|
589
|
+
const parent = layoutParent(node);
|
|
590
|
+
if (!parent)
|
|
591
|
+
return;
|
|
592
|
+
const container = containerOf(parent);
|
|
593
|
+
// Remembered on the container, so the box is merged again when the direction changes.
|
|
594
|
+
parent.fitContainer = container;
|
|
595
|
+
delete props['flexBasis'];
|
|
596
|
+
if (container.startsWith('row'))
|
|
597
|
+
props['width'] = basis;
|
|
598
|
+
else {
|
|
599
|
+
props['height'] = basis;
|
|
600
|
+
percentHeight(node, props);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Whether a box has a height a percentage can be taken of, as CSS defines one: a height of its
|
|
605
|
+
* own, or one the layout around it gives it. A box as tall as what it holds has none.
|
|
606
|
+
*
|
|
607
|
+
* Asked of each box on the way up, and remembered on it, so a change to any of them works out
|
|
608
|
+
* again the boxes beneath it. Where the answer is not known for certain it is yes, which leaves
|
|
609
|
+
* the percentage to Yoga as before: a view native sizes itself, or one placed out of the flow.
|
|
610
|
+
*/
|
|
611
|
+
function definiteHeight(node) {
|
|
612
|
+
return (node.heightBasis = hasDefiniteHeight(node));
|
|
613
|
+
}
|
|
614
|
+
function hasDefiniteHeight(node) {
|
|
615
|
+
// The root is the screen, and only a plain view is sized by its content alone.
|
|
616
|
+
if (!node.parent || node.kind !== 'element' || viewNameOf(node) !== 'View')
|
|
617
|
+
return true;
|
|
618
|
+
const parent = layoutParent(node);
|
|
619
|
+
if (!parent)
|
|
620
|
+
return true;
|
|
621
|
+
const height = ownLayout(node, 'height');
|
|
622
|
+
if (isPercent(height))
|
|
623
|
+
return definiteHeight(parent);
|
|
624
|
+
return sizesItself(node, height) || heightFromLayout(node, parent);
|
|
625
|
+
}
|
|
626
|
+
const NO_HEIGHT = new Set([undefined, null, 'auto', 'fit-content']);
|
|
627
|
+
/** Whether a box has a height from its own style: written, or placed, or kept in proportion. */
|
|
628
|
+
function sizesItself(node, height) {
|
|
629
|
+
if (!NO_HEIGHT.has(height))
|
|
630
|
+
return true;
|
|
631
|
+
return ownLayout(node, 'position') === 'absolute' || ownLayout(node, 'aspectRatio') != null;
|
|
632
|
+
}
|
|
633
|
+
/** Whether the container a box is in gives it a height: a row's, or a share of a column's. */
|
|
634
|
+
function heightFromLayout(node, parent) {
|
|
635
|
+
const [direction, align] = containerOf(parent).split(' ');
|
|
636
|
+
if (direction === 'row') {
|
|
637
|
+
// Stretched across a row, it is as tall as the row, where the row has a height to be. One
|
|
638
|
+
// as tall as its content gives none: Yoga takes the percentage of the space on offer there
|
|
639
|
+
// too, the screen's, where a browser takes it of the row once its content has sized it.
|
|
640
|
+
const self = ownLayout(node, 'alignSelf');
|
|
641
|
+
return STRETCHES.has(self) && align === 'stretch' && definiteHeight(parent);
|
|
642
|
+
}
|
|
643
|
+
// In a column it has the height it grows to, where the column has one to share out.
|
|
644
|
+
const grows = Number(ownLayout(node, 'flexGrow') ?? ownLayout(node, 'flex') ?? 0) > 0;
|
|
645
|
+
return grows && definiteHeight(parent);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* A placeholder's opacity, which native has none for, as its colour faded by that share: none of
|
|
649
|
+
* it at 0, which is how a stylesheet hides a placeholder under a label, and all of it at 1.
|
|
650
|
+
*
|
|
651
|
+
* ponytail: with no colour of its own the placeholder is native's default, and a platform colour
|
|
652
|
+
* is native's to resolve: neither is known here, so each is hidden at 0 and left as it is above
|
|
653
|
+
* that. Name the platform's default, or read the colour back, if one is faded part of the way.
|
|
654
|
+
*/
|
|
655
|
+
function placeholderFaded(props) {
|
|
656
|
+
const opacity = props['placeholderOpacity'];
|
|
657
|
+
if (opacity === undefined)
|
|
658
|
+
return;
|
|
659
|
+
delete props['placeholderOpacity'];
|
|
660
|
+
if (typeof opacity !== 'number' || opacity >= 1)
|
|
661
|
+
return;
|
|
662
|
+
const colour = props['placeholderTextColor'];
|
|
663
|
+
if (typeof colour === 'string')
|
|
664
|
+
props['placeholderTextColor'] = faded(colour, Math.max(opacity, 0));
|
|
665
|
+
else if (opacity <= 0)
|
|
666
|
+
props['placeholderTextColor'] = 'rgba(0, 0, 0, 0)';
|
|
667
|
+
}
|
|
668
|
+
/** The least alpha iOS sends a touch to a view at: under a hundredth it is passed over. */
|
|
669
|
+
const TOUCHED_ALPHA = 0.011;
|
|
670
|
+
/**
|
|
671
|
+
* A view that takes a touch and has no opacity, committed with the least there is to be sent
|
|
672
|
+
* one. A browser sends a press to an element at `opacity: 0`, which is how a see-through
|
|
673
|
+
* backdrop hears a press outside a menu; iOS passes over a view it would not draw. A hundredth
|
|
674
|
+
* of what the view draws is not seen.
|
|
675
|
+
*/
|
|
676
|
+
function stillTouched(props) {
|
|
677
|
+
// One told to take no touch is not to be sent one, seen or not.
|
|
678
|
+
if (props['pointerEvents'] === 'none')
|
|
679
|
+
return;
|
|
680
|
+
const opacity = props['opacity'];
|
|
681
|
+
if (typeof opacity === 'number' && opacity < TOUCHED_ALPHA)
|
|
682
|
+
props['opacity'] = TOUCHED_ALPHA;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Leave `aspect-ratio` out for a box with both a width and a height. CSS takes the ratio only
|
|
686
|
+
* for a size that is `auto`; Yoga takes it over one of two that are given, the height in a
|
|
687
|
+
* column and the width in a row.
|
|
688
|
+
*/
|
|
689
|
+
function ratioForAutoSize(props) {
|
|
690
|
+
if (!isSet(props['aspectRatio']))
|
|
691
|
+
return;
|
|
692
|
+
const given = (size) => isSet(size) && size !== 'auto';
|
|
693
|
+
if (given(props['width']) && given(props['height']))
|
|
694
|
+
delete props['aspectRatio'];
|
|
695
|
+
}
|
|
696
|
+
const OUTLINE_KEYS = ['outlineWidth', 'outlineStyle', 'outlineColor', 'outlineOffset'];
|
|
697
|
+
/**
|
|
698
|
+
* Send nothing for an outline of no width, which is what `outline: none` is: it draws nothing,
|
|
699
|
+
* and a view with no outline is the same view. Sent, it is an update that is not one, and on
|
|
700
|
+
* Android a text field given an outline prop after it is first drawn has its background set
|
|
701
|
+
* again, with which the padding Android gives a text field comes back over the padding it was
|
|
702
|
+
* laid out with: a field as tall as its line clips its text away. A stylesheet takes a
|
|
703
|
+
* browser's focus ring off a field with `outline: none` for the focused field alone.
|
|
704
|
+
*/
|
|
705
|
+
function noOutline(props) {
|
|
706
|
+
if (props['outlineWidth'] !== 0)
|
|
707
|
+
return;
|
|
708
|
+
for (const key of OUTLINE_KEYS)
|
|
709
|
+
delete props[key];
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* A box with `display: none`, out of the flow as well. It takes no room either way, but Yoga
|
|
713
|
+
* reads an item's baseline from its first child that is in the flow, and a hidden one has no
|
|
714
|
+
* layout to read: under a row with `align-items: baseline` the baseline is not a number, and
|
|
715
|
+
* the row comes to no height with nothing in it placed. Out of the flow it is passed over.
|
|
716
|
+
*/
|
|
717
|
+
function hiddenOutOfFlow(props) {
|
|
718
|
+
if (props['display'] === 'none')
|
|
719
|
+
props['position'] = 'absolute';
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* `height: 50%` under a parent with no height to take it of, which CSS reads as `auto`. Yoga
|
|
723
|
+
* takes it of the space on offer, the screen's or everything a scroll view holds, so no height
|
|
724
|
+
* is sent.
|
|
725
|
+
*/
|
|
726
|
+
function percentHeight(node, props) {
|
|
727
|
+
if (!isPercent(props['height']))
|
|
728
|
+
return;
|
|
729
|
+
// Out of the flow, it is of the box that holds it, which is laid out first and has a height.
|
|
730
|
+
if (props['position'] === 'absolute')
|
|
731
|
+
return;
|
|
732
|
+
const parent = layoutParent(node);
|
|
733
|
+
if (parent && !definiteHeight(parent))
|
|
734
|
+
delete props['height'];
|
|
735
|
+
}
|
|
462
736
|
/**
|
|
463
737
|
* Start a multiline Android text field's text at the top, as iOS and a `<textarea>` do.
|
|
464
738
|
*
|
|
@@ -504,6 +778,43 @@ const TEXT_ALIGN = {
|
|
|
504
778
|
};
|
|
505
779
|
/** The native modal host, which is left out of a commit while `visible` is false. */
|
|
506
780
|
const MODAL_HOST = 'ModalHostView';
|
|
781
|
+
/**
|
|
782
|
+
* A screen's `gestureResponseDistance` that no touch is within: how far from each edge a swipe
|
|
783
|
+
* back may start, with -1 for no limit, and none at all from the leading edge. It is what
|
|
784
|
+
* `react-native-screens` reads as the swipe is about to begin; whether the swipe is enabled it
|
|
785
|
+
* read when the finger came down, before anything here had heard of the touch.
|
|
786
|
+
*/
|
|
787
|
+
const NO_SWIPE = Object.freeze({ start: -1, end: 0, top: -1, bottom: -1 });
|
|
788
|
+
/** A screen of a native stack, which is what the swipe back is a gesture of. */
|
|
789
|
+
const STACK_SCREEN = 'RNSScreen';
|
|
790
|
+
/** What an element's `touch-action` is: bound on it, or from the rules it matches. */
|
|
791
|
+
function touchActionOf(node) {
|
|
792
|
+
return inlineOf(node)['touchAction'] ?? node.styleCache?.style['touchAction'];
|
|
793
|
+
}
|
|
794
|
+
/** Where a touch is on the screen: its own point, or its first finger's. */
|
|
795
|
+
function pointOf(nativeEvent) {
|
|
796
|
+
const event = (nativeEvent ?? {});
|
|
797
|
+
const touch = event.pageX === undefined ? event.touches?.[0] : event;
|
|
798
|
+
return { x: touch?.pageX ?? 0, y: touch?.pageY ?? 0 };
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* What a touch on a node leaves to the browser: the `touch-action` of it, or of the nearest
|
|
802
|
+
* element over it that says. Nothing where none does, which leaves everything.
|
|
803
|
+
*/
|
|
804
|
+
function touchActions(node) {
|
|
805
|
+
for (let up = node; up; up = up.parent) {
|
|
806
|
+
const said = up.kind === 'element' ? touchActionOf(up) : undefined;
|
|
807
|
+
if (typeof said === 'string')
|
|
808
|
+
return said.split(' ');
|
|
809
|
+
}
|
|
810
|
+
return undefined;
|
|
811
|
+
}
|
|
812
|
+
const PANS_ACROSS = ['auto', 'manipulation', 'pan-x', 'pan-left', 'pan-right'];
|
|
813
|
+
const PANS_ALONG = ['auto', 'manipulation', 'pan-y', 'pan-up', 'pan-down'];
|
|
814
|
+
/** Whether an element keeps a drag in one direction: its `touch-action` leaves no pan that way. */
|
|
815
|
+
const keeps = (actions, pans) => actions !== undefined && !actions.some((action) => pans.includes(action));
|
|
816
|
+
/** How far a finger moves before a browser has settled which way a drag is going. */
|
|
817
|
+
const DRAG_SLOP = 4;
|
|
507
818
|
/**
|
|
508
819
|
* Register a codegen'd third-party Fabric component, e.g.
|
|
509
820
|
* `registerViewName('rns-screen', 'RNSScreen')`, which is how the router reaches
|
|
@@ -762,13 +1073,45 @@ function isTextElement(node) {
|
|
|
762
1073
|
return false;
|
|
763
1074
|
return !TEXT_ELEMENTS.has(node.name) || holdsOnlyText(node);
|
|
764
1075
|
}
|
|
1076
|
+
/** Whether a text element holds text and nothing else. One that holds nothing holds no text. */
|
|
765
1077
|
function holdsOnlyText(node) {
|
|
766
|
-
|
|
1078
|
+
const children = node.children ?? [];
|
|
1079
|
+
for (const child of children) {
|
|
767
1080
|
if (child.kind === 'element' && !isTextElement(child))
|
|
768
1081
|
return false;
|
|
769
1082
|
}
|
|
770
|
-
return
|
|
1083
|
+
return children.length > 0;
|
|
771
1084
|
}
|
|
1085
|
+
const ALIGNS = new Set(['center', 'flex-end', 'space-around', 'space-evenly']);
|
|
1086
|
+
/**
|
|
1087
|
+
* Whether one of HTML's text elements is a flex container that aligns the one run of text it
|
|
1088
|
+
* holds, as an avatar's initials are centred: `<span class="flex items-center justify-center">`.
|
|
1089
|
+
*
|
|
1090
|
+
* A browser makes the run a flex item and places it. A paragraph's text is its content, which no
|
|
1091
|
+
* alignment moves, so such an element commits as a view, and its text as the paragraph any view's
|
|
1092
|
+
* loose text is. Read from the node's own style on each reconcile, which a change to it brings
|
|
1093
|
+
* about, so nothing is kept to go stale. The run can be a text element of its own, a label in a
|
|
1094
|
+
* span, which is then the item placed. A `<text>` is a paragraph whatever its style, and an
|
|
1095
|
+
* element holding more than the one run is left as it was.
|
|
1096
|
+
*/
|
|
1097
|
+
function aligningText(node) {
|
|
1098
|
+
if (node.kind !== 'element' || node.name === 'text' || !TEXT_ELEMENTS.has(node.name)) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
if (node.children.length !== 1 || !oneRun(node.children[0]))
|
|
1102
|
+
return false;
|
|
1103
|
+
// Under a text element it is a span of that paragraph, unless that one is a view placing it.
|
|
1104
|
+
if (isTextElement(node.parent) && !aligningText(node.parent))
|
|
1105
|
+
return false;
|
|
1106
|
+
if (ownLayout(node, 'display') !== 'flex')
|
|
1107
|
+
return false;
|
|
1108
|
+
return (ALIGNS.has(ownLayout(node, 'alignItems')) ||
|
|
1109
|
+
ALIGNS.has(ownLayout(node, 'justifyContent')));
|
|
1110
|
+
}
|
|
1111
|
+
/** One run of text to place: text itself, or a text element around some, a label in a span. */
|
|
1112
|
+
const oneRun = (child) => child.kind === 'text' || (child.kind === 'element' && TEXT_ELEMENTS.has(child.name));
|
|
1113
|
+
/** The view a node is committed as: `viewNameOf`, but for a text element that aligns its text. */
|
|
1114
|
+
const committedViewName = (node) => aligningText(node) ? DEFAULT_VIEW : viewNameOf(node);
|
|
772
1115
|
/**
|
|
773
1116
|
* The native view a node commits as: `Paragraph` for a `<text>`, `VirtualText` for one nested in
|
|
774
1117
|
* another, `RawText` for a run of text, and `View` for anything unregistered, which is what an
|
|
@@ -795,8 +1138,17 @@ export function viewNameOf(node) {
|
|
|
795
1138
|
if (node.name !== 'text' && TEXT_ELEMENTS.has(node.name) && !holdsOnlyText(node)) {
|
|
796
1139
|
return DEFAULT_VIEW;
|
|
797
1140
|
}
|
|
798
|
-
|
|
1141
|
+
// The paragraph the engine makes for a view's loose text is under a view, whatever the name
|
|
1142
|
+
// of the element that view is.
|
|
1143
|
+
return nestedText(node) ? VIRTUAL_TEXT : PARAGRAPH;
|
|
799
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Whether a text element is a span of the paragraph above it: under another text element, but
|
|
1147
|
+
* not one that is a view for placing this one (see `aligningText`), nor the engine's own box.
|
|
1148
|
+
*/
|
|
1149
|
+
const nestedText = (node) => !node.anonymous && isTextElement(node.parent) && !placesText(node.parent);
|
|
1150
|
+
/** Whether a node's parent is a text element committed as a view, to place the run it holds. */
|
|
1151
|
+
const placesText = (parent) => parent != null && 'props' in parent && aligningText(parent);
|
|
800
1152
|
/**
|
|
801
1153
|
* The props every native view reads as a boolean: the `bool` fields of React Native's
|
|
802
1154
|
* `BaseViewProps`, `AccessibilityProps` and `HostPlatformViewProps`, by the names JavaScript sends.
|
|
@@ -835,33 +1187,54 @@ const BOOLEAN_VIEW_PROPS = new Set([
|
|
|
835
1187
|
]);
|
|
836
1188
|
/**
|
|
837
1189
|
* Whether `node` holds nothing `:empty` sees besides `moved`: so it was empty before `moved` came,
|
|
838
|
-
* or is now that it has gone.
|
|
1190
|
+
* or is now that it has gone. What `:empty` does not see is `fills` in css.ts to say.
|
|
839
1191
|
*/
|
|
840
1192
|
function wasOrIsEmptyWithout(node, moved) {
|
|
841
|
-
if (moved
|
|
1193
|
+
if (!fills(moved))
|
|
842
1194
|
return false;
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1195
|
+
return !node.children.some((child) => child !== moved && fills(child));
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Have the child that paints a node's background merged again with the node: nothing of the
|
|
1199
|
+
* child's own moved, and what it paints may have.
|
|
1200
|
+
*/
|
|
1201
|
+
function repaint(node) {
|
|
1202
|
+
if (node.paintsOn?.parent === node)
|
|
1203
|
+
node.paintsOn.propsDirty = true;
|
|
848
1204
|
}
|
|
1205
|
+
/** A sixtieth of a second: how far apart the frames native is given for an animation are. */
|
|
1206
|
+
const NATIVE_FRAME = 1000 / 60;
|
|
1207
|
+
/** The style keys that are a view's background. */
|
|
1208
|
+
const PAINT_KEYS = [
|
|
1209
|
+
'backgroundColor',
|
|
1210
|
+
'experimental_backgroundImage',
|
|
1211
|
+
'experimental_backgroundSize',
|
|
1212
|
+
'experimental_backgroundPosition',
|
|
1213
|
+
'experimental_backgroundRepeat',
|
|
1214
|
+
];
|
|
849
1215
|
/**
|
|
850
1216
|
* A node's own props onto what it commits with. No native prop has a hyphen: `data-*` and `aria-*`
|
|
851
1217
|
* attributes stay on the node for selectors to match, and an `aria-*` one is mapped to the prop
|
|
852
|
-
* native reads.
|
|
1218
|
+
* native reads. The node's `attributeOnly` names stay on it the same way.
|
|
853
1219
|
*/
|
|
854
|
-
function writeOwnProps(own, props) {
|
|
1220
|
+
function writeOwnProps(own, props, attributeOnly) {
|
|
855
1221
|
let aria = false;
|
|
856
1222
|
for (const key of Object.keys(own)) {
|
|
1223
|
+
if (attributeOnly?.has(key))
|
|
1224
|
+
continue;
|
|
857
1225
|
if (key.includes('-'))
|
|
858
1226
|
aria ||= key.startsWith('aria-');
|
|
859
1227
|
else if (key !== 'style' && key !== INTRINSIC_SIZE && key !== STYLE_OVERRIDE) {
|
|
860
1228
|
props[key] = committedProp(key, own[key]);
|
|
861
1229
|
}
|
|
862
1230
|
}
|
|
863
|
-
if (aria)
|
|
864
|
-
|
|
1231
|
+
if (!aria)
|
|
1232
|
+
return;
|
|
1233
|
+
// An attribute-only `aria-*` is not mapped to the prop native reads either.
|
|
1234
|
+
const mapped = attributeOnly?.size
|
|
1235
|
+
? Object.fromEntries(Object.entries(own).filter(([key]) => !attributeOnly.has(key)))
|
|
1236
|
+
: own;
|
|
1237
|
+
applyAria(mapped, props);
|
|
865
1238
|
}
|
|
866
1239
|
/** A prop as it is committed: a boolean view prop held as text is the boolean the text says. */
|
|
867
1240
|
function committedProp(key, value) {
|
|
@@ -1039,6 +1412,10 @@ class RetainedNode {
|
|
|
1039
1412
|
styleCache = null;
|
|
1040
1413
|
styleDirty = true;
|
|
1041
1414
|
styleCommitted = null;
|
|
1415
|
+
fitContainer = undefined;
|
|
1416
|
+
heightBasis = undefined;
|
|
1417
|
+
hasDirty = false;
|
|
1418
|
+
stateDirty = false;
|
|
1042
1419
|
ownStyle = undefined;
|
|
1043
1420
|
claimed = undefined;
|
|
1044
1421
|
dormantHoists = null;
|
|
@@ -1182,12 +1559,19 @@ class RetainedNode {
|
|
|
1182
1559
|
* colours - `shouldRasterizeIOS` is a boolean.
|
|
1183
1560
|
*/
|
|
1184
1561
|
const IS_COLOR_PROP = /color(android)?$/i;
|
|
1562
|
+
/**
|
|
1563
|
+
* Colour props React Native does not name with a `color` suffix: `<switch>`'s pair ends in the
|
|
1564
|
+
* state each one paints - `trackColorForTrue` and `trackColorForFalse`. They are the same type
|
|
1565
|
+
* problem the `Android` suffix causes: Android's `ColorPropConverter` takes a number or a
|
|
1566
|
+
* platform-colour map, refuses a string, and throws rather than approximating one.
|
|
1567
|
+
*/
|
|
1568
|
+
const STATE_COLOR_PROPS = new Set(['trackColorForTrue', 'trackColorForFalse']);
|
|
1185
1569
|
/** Answers per key name, once: the regex ran for every prop of every node in every commit. */
|
|
1186
1570
|
const colorProps = new Map();
|
|
1187
1571
|
function isColorProp(key) {
|
|
1188
1572
|
let answer = colorProps.get(key);
|
|
1189
1573
|
if (answer === undefined) {
|
|
1190
|
-
colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || BRUSH_PROPS.has(key)));
|
|
1574
|
+
colorProps.set(key, (answer = IS_COLOR_PROP.test(key) || STATE_COLOR_PROPS.has(key) || BRUSH_PROPS.has(key)));
|
|
1191
1575
|
}
|
|
1192
1576
|
return answer;
|
|
1193
1577
|
}
|
|
@@ -1431,6 +1815,16 @@ export class Engine {
|
|
|
1431
1815
|
* does, a child list can move without anything else needing to be looked at again.
|
|
1432
1816
|
*/
|
|
1433
1817
|
structuralSheets = false;
|
|
1818
|
+
/** How many children after a change in a list one of them reaches. See `siblingReach`. */
|
|
1819
|
+
structuralAfter = 0;
|
|
1820
|
+
/** Whether one of them reaches the children before it. */
|
|
1821
|
+
structuralBefore = false;
|
|
1822
|
+
/** The compounds of theirs that read an element's place from under it. See `markPlace`. */
|
|
1823
|
+
placeElsewhere = new Set();
|
|
1824
|
+
/** The sheets `watchStructure` has read, which it is asked about once an element. */
|
|
1825
|
+
watchedStructure = new WeakSet();
|
|
1826
|
+
/** Whether any sheet uses `:has()`. See `markBeneath`. */
|
|
1827
|
+
hasSheets = false;
|
|
1434
1828
|
resolveAssetSource;
|
|
1435
1829
|
constructor(fabric, rootTag, options = {}) {
|
|
1436
1830
|
this.fabric = fabric;
|
|
@@ -1441,7 +1835,9 @@ export class Engine {
|
|
|
1441
1835
|
this.fontScale = conditions.fontScale;
|
|
1442
1836
|
if (options.tokens)
|
|
1443
1837
|
this.styles.setRootTokens(options.tokens);
|
|
1444
|
-
this.
|
|
1838
|
+
this.watchStructure(options.globalStyles);
|
|
1839
|
+
this.watchHas(options.globalStyles);
|
|
1840
|
+
this.watchActive(options.globalStyles);
|
|
1445
1841
|
this.dev = options.dev ?? globalThis.__DEV__ === true;
|
|
1446
1842
|
if (this.dev) {
|
|
1447
1843
|
this.styles.onUndefinedToken = (name, props, on) => this.reportUndefinedToken(name, props, on);
|
|
@@ -1506,11 +1902,70 @@ export class Engine {
|
|
|
1506
1902
|
if (node.active === active)
|
|
1507
1903
|
continue;
|
|
1508
1904
|
node.active = active;
|
|
1509
|
-
this.
|
|
1905
|
+
this.markActive(node);
|
|
1510
1906
|
changed = true;
|
|
1511
1907
|
}
|
|
1512
1908
|
return changed;
|
|
1513
1909
|
}
|
|
1910
|
+
/** Whether any sheet styles a pressed element, and each compound that asks about one from another. */
|
|
1911
|
+
activeUse = {
|
|
1912
|
+
own: false,
|
|
1913
|
+
elsewhere: [],
|
|
1914
|
+
};
|
|
1915
|
+
/** What each sheet in play asks about a pressed element: `activeUse` is all of them together. */
|
|
1916
|
+
activeBySheet = new Map();
|
|
1917
|
+
/** Note what a sheet in play asks about a pressed element. See `markActive`. */
|
|
1918
|
+
watchActive(sheet) {
|
|
1919
|
+
// Asked for every element a component's sheet is given to: noted once a sheet.
|
|
1920
|
+
if (!sheet || this.activeBySheet.has(sheet))
|
|
1921
|
+
return;
|
|
1922
|
+
const use = usesActive(sheet);
|
|
1923
|
+
this.activeBySheet.set(sheet, use);
|
|
1924
|
+
if (!use.own && !use.elsewhere.length)
|
|
1925
|
+
return;
|
|
1926
|
+
this.countActive();
|
|
1927
|
+
if (this.styles.tracksHas)
|
|
1928
|
+
return;
|
|
1929
|
+
// What a pressed element is compared with is the rules it matched, which no cache kept until
|
|
1930
|
+
// a sheet asked. Nothing is resolved yet when the sheet is the one the engine was made with.
|
|
1931
|
+
this.styles.tracksHas = true;
|
|
1932
|
+
if (!this.root)
|
|
1933
|
+
return;
|
|
1934
|
+
this.root.styleDirty = true;
|
|
1935
|
+
this.markPath(this.root);
|
|
1936
|
+
}
|
|
1937
|
+
/** A sheet has left play, by a hot swap or by being removed: what it asked goes with it. */
|
|
1938
|
+
unwatchActive(sheet) {
|
|
1939
|
+
if (this.activeBySheet.delete(sheet))
|
|
1940
|
+
this.countActive();
|
|
1941
|
+
}
|
|
1942
|
+
countActive() {
|
|
1943
|
+
const all = [...this.activeBySheet.values()];
|
|
1944
|
+
this.activeUse = {
|
|
1945
|
+
own: all.some((use) => use.own),
|
|
1946
|
+
elsewhere: all.flatMap((use) => use.elsewhere),
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* A press began or ended on `node` or under it, which is all that changed about it. A press
|
|
1951
|
+
* reaches every ancestor up to the root, and marking each as restyled would style the whole
|
|
1952
|
+
* screen again, twice a press. So each is only matched again, and keeps its style and all
|
|
1953
|
+
* that is under it unless it now matches other rules. An element a rule asks about from
|
|
1954
|
+
* inside or beside it, `.card:active .title`, changes more than itself, and is restyled with
|
|
1955
|
+
* everything under it as any other change is. With no rule for a pressed element at all there
|
|
1956
|
+
* is nothing to match.
|
|
1957
|
+
*/
|
|
1958
|
+
markActive(node) {
|
|
1959
|
+
const { own, elsewhere } = this.activeUse;
|
|
1960
|
+
if (!own && !elsewhere.length)
|
|
1961
|
+
return;
|
|
1962
|
+
// By its classes alone: a compound with none could be any element.
|
|
1963
|
+
const asked = elsewhere.some((compound) => compound.classes.every((name) => node.classes?.has(name)));
|
|
1964
|
+
if (asked)
|
|
1965
|
+
return this.markProps(node);
|
|
1966
|
+
node.stateDirty = true;
|
|
1967
|
+
this.markPath(node.parent ?? node);
|
|
1968
|
+
}
|
|
1514
1969
|
/**
|
|
1515
1970
|
* Custom properties the device knows and the stylesheet cannot, once they change.
|
|
1516
1971
|
*
|
|
@@ -1531,11 +1986,26 @@ export class Engine {
|
|
|
1531
1986
|
this.sheetReplaced(replacing, sheet);
|
|
1532
1987
|
else
|
|
1533
1988
|
this.registerSheet(sheet);
|
|
1534
|
-
|
|
1535
|
-
|
|
1989
|
+
this.watchStructure(sheet);
|
|
1990
|
+
this.watchHas(sheet);
|
|
1991
|
+
this.watchActive(sheet);
|
|
1536
1992
|
this.markPath(this.root);
|
|
1993
|
+
this.markWrittenFor(sheet, this.root);
|
|
1537
1994
|
return true;
|
|
1538
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Have styled again each node a sheet just added could match. The rest keep the style they
|
|
1998
|
+
* have, which the resolver lets stand for a node none of the sheet's rules is written for,
|
|
1999
|
+
* so a commit steps over them: the walk here is what finds the ones it must not step over.
|
|
2000
|
+
*/
|
|
2001
|
+
markWrittenFor(sheet, node) {
|
|
2002
|
+
if (node.kind === 'element' && this.styles.couldMatch(sheet, node)) {
|
|
2003
|
+
node.styleDirty = true;
|
|
2004
|
+
this.markPath(node);
|
|
2005
|
+
}
|
|
2006
|
+
for (const child of node.children)
|
|
2007
|
+
this.markWrittenFor(sheet, child);
|
|
2008
|
+
}
|
|
1539
2009
|
/**
|
|
1540
2010
|
* Stop matching a sheet `addGlobalSheet` added, and restyle everything; false, and nothing
|
|
1541
2011
|
* done, when it is not one. A hot swap that leaves a None component no rules, or makes it scoped.
|
|
@@ -1563,14 +2033,19 @@ export class Engine {
|
|
|
1563
2033
|
* which is what makes Fabric dirty a measured node and measure it at the size now in effect.
|
|
1564
2034
|
*/
|
|
1565
2035
|
remeasureText() {
|
|
2036
|
+
const again = (measured) => {
|
|
2037
|
+
measured.remeasure = true;
|
|
2038
|
+
this.markProps(measured, false);
|
|
2039
|
+
};
|
|
1566
2040
|
const visit = (node) => {
|
|
1567
2041
|
for (const child of node.children) {
|
|
2042
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
2043
|
+
if (child.kind === 'text' && child.box?.committed)
|
|
2044
|
+
again(child.box);
|
|
1568
2045
|
if (child.kind !== 'element')
|
|
1569
2046
|
continue;
|
|
1570
|
-
if (child.committed && MEASURED_VIEWS.has(
|
|
1571
|
-
child
|
|
1572
|
-
this.markProps(child, false);
|
|
1573
|
-
}
|
|
2047
|
+
if (child.committed && MEASURED_VIEWS.has(committedViewName(child)))
|
|
2048
|
+
again(child);
|
|
1574
2049
|
visit(child);
|
|
1575
2050
|
}
|
|
1576
2051
|
};
|
|
@@ -1716,8 +2191,10 @@ export class Engine {
|
|
|
1716
2191
|
if (sheet === undefined || node.sheet === sheet)
|
|
1717
2192
|
return;
|
|
1718
2193
|
node.sheet = sheet;
|
|
1719
|
-
|
|
1720
|
-
|
|
2194
|
+
this.styles.noteSheet(sheet);
|
|
2195
|
+
this.watchStructure(sheet);
|
|
2196
|
+
this.watchHas(sheet);
|
|
2197
|
+
this.watchActive(sheet);
|
|
1721
2198
|
this.markProps(node);
|
|
1722
2199
|
}
|
|
1723
2200
|
/** The sheet each node given another's scope was created with. */
|
|
@@ -1750,25 +2227,61 @@ export class Engine {
|
|
|
1750
2227
|
createElement(name, sheet = null) {
|
|
1751
2228
|
const node = new RetainedNode('element', name, this);
|
|
1752
2229
|
node.sheet = sheet;
|
|
2230
|
+
this.styles.noteSheet(sheet);
|
|
1753
2231
|
const html = HTML_ELEMENTS.get(name);
|
|
1754
2232
|
if (html !== undefined) {
|
|
1755
2233
|
if (html.role)
|
|
1756
2234
|
node.props['accessibilityRole'] = html.role;
|
|
1757
2235
|
node.styled = html.styled;
|
|
1758
2236
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
2237
|
+
this.watchStructure(sheet);
|
|
2238
|
+
this.watchHas(sheet);
|
|
2239
|
+
this.watchActive(sheet);
|
|
1761
2240
|
if (HOISTS[name])
|
|
1762
2241
|
this.hoisted.add(node);
|
|
1763
2242
|
return node;
|
|
1764
2243
|
}
|
|
1765
2244
|
addClass(node, name) {
|
|
1766
2245
|
(node.classes ??= new Set()).add(name);
|
|
1767
|
-
this.
|
|
2246
|
+
this.markClasses(node, [name]);
|
|
1768
2247
|
}
|
|
1769
2248
|
removeClass(node, name) {
|
|
1770
2249
|
node.classes?.delete(name);
|
|
1771
|
-
this.
|
|
2250
|
+
this.markClasses(node, [name]);
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Mark a node whose classes changed, by what the rules that name those classes can restyle.
|
|
2254
|
+
*
|
|
2255
|
+
* A class no selector names changes no style: a library's own marker, of an overlay that is
|
|
2256
|
+
* animating or a control that was touched. One named only as what a rule's element is
|
|
2257
|
+
* inside, `.animating .close`, restyles the elements under the node that such rules are for.
|
|
2258
|
+
* Either way a whole subtree is not styled again for a class that has no say in it. Any other
|
|
2259
|
+
* class restyles the node and everything under it, as any other change to it does.
|
|
2260
|
+
*
|
|
2261
|
+
* A sheet that names a class later restyles what it is for as it arrives, and a node not yet
|
|
2262
|
+
* committed is styled when it is, with the class as it is then.
|
|
2263
|
+
*/
|
|
2264
|
+
markClasses(node, changed) {
|
|
2265
|
+
const reach = this.styles.reachOf(changed);
|
|
2266
|
+
if (reach === true)
|
|
2267
|
+
return this.markProps(node);
|
|
2268
|
+
this.markProps(node, false);
|
|
2269
|
+
if (reach)
|
|
2270
|
+
this.markUnder(node, reach);
|
|
2271
|
+
}
|
|
2272
|
+
/** Have styled again each element under a node that one of `subjects`' rules could be for. */
|
|
2273
|
+
markUnder(node, subjects) {
|
|
2274
|
+
for (const child of node.children) {
|
|
2275
|
+
if (child.kind !== 'element')
|
|
2276
|
+
continue;
|
|
2277
|
+
if (this.styles.isFor(subjects, child)) {
|
|
2278
|
+
// And with it all under it, which is styled from it.
|
|
2279
|
+
child.styleDirty = true;
|
|
2280
|
+
this.markPath(child);
|
|
2281
|
+
}
|
|
2282
|
+
else
|
|
2283
|
+
this.markUnder(child, subjects);
|
|
2284
|
+
}
|
|
1772
2285
|
}
|
|
1773
2286
|
/**
|
|
1774
2287
|
* The sheet of the component `node` hosts, whose `:host` rules it matches. Set again when a hot
|
|
@@ -1779,6 +2292,9 @@ export class Engine {
|
|
|
1779
2292
|
if (node.hostSheet === sheet)
|
|
1780
2293
|
return;
|
|
1781
2294
|
node.hostSheet = sheet;
|
|
2295
|
+
this.styles.noteSheet(sheet);
|
|
2296
|
+
this.watchStructure(sheet);
|
|
2297
|
+
this.watchActive(sheet);
|
|
1782
2298
|
this.markProps(node);
|
|
1783
2299
|
}
|
|
1784
2300
|
/**
|
|
@@ -1793,8 +2309,14 @@ export class Engine {
|
|
|
1793
2309
|
/** `class="a b"` from a template. Replaces the set rather than adding to it. */
|
|
1794
2310
|
setClasses(node, value) {
|
|
1795
2311
|
const names = value.split(/\s+/).filter(Boolean);
|
|
2312
|
+
const before = node.classes;
|
|
1796
2313
|
node.classes = names.length ? new Set(names) : null;
|
|
1797
|
-
|
|
2314
|
+
// Each class that came or went: one in both changes nothing.
|
|
2315
|
+
const changed = [...names.filter((name) => !before?.has(name))];
|
|
2316
|
+
for (const name of before ?? [])
|
|
2317
|
+
if (!node.classes?.has(name))
|
|
2318
|
+
changed.push(name);
|
|
2319
|
+
this.markClasses(node, changed);
|
|
1798
2320
|
}
|
|
1799
2321
|
/** Angular's `@if`/`@for`/`ViewContainerRef` markers. Ordered, never committed. */
|
|
1800
2322
|
createAnchor() {
|
|
@@ -1811,7 +2333,7 @@ export class Engine {
|
|
|
1811
2333
|
parent.children.push(child);
|
|
1812
2334
|
if (child.dormantHoists)
|
|
1813
2335
|
this.wakeHoists(child);
|
|
1814
|
-
this.markStructure(parent, child);
|
|
2336
|
+
this.markStructure(parent, child, parent.children.length - 1);
|
|
1815
2337
|
this.markTextContent(parent, child);
|
|
1816
2338
|
}
|
|
1817
2339
|
insertBefore(parent, child, ref) {
|
|
@@ -1824,7 +2346,7 @@ export class Engine {
|
|
|
1824
2346
|
parent.children.splice(at, 0, child);
|
|
1825
2347
|
if (child.dormantHoists)
|
|
1826
2348
|
this.wakeHoists(child);
|
|
1827
|
-
this.markStructure(parent, child);
|
|
2349
|
+
this.markStructure(parent, child, at < 0 ? parent.children.length - 1 : at);
|
|
1828
2350
|
this.markTextContent(parent, child);
|
|
1829
2351
|
}
|
|
1830
2352
|
removeChild(parent, child) {
|
|
@@ -1837,7 +2359,7 @@ export class Engine {
|
|
|
1837
2359
|
target.children.splice(at, 1);
|
|
1838
2360
|
child.parent = null;
|
|
1839
2361
|
this.removedSinceCommit.add(child);
|
|
1840
|
-
this.markStructure(target, child);
|
|
2362
|
+
this.markStructure(target, child, at);
|
|
1841
2363
|
this.markTextContent(target, child);
|
|
1842
2364
|
}
|
|
1843
2365
|
/**
|
|
@@ -1894,6 +2416,7 @@ export class Engine {
|
|
|
1894
2416
|
node.playing = undefined;
|
|
1895
2417
|
this.markProps(node, false);
|
|
1896
2418
|
}
|
|
2419
|
+
this.releasePlayedNatively();
|
|
1897
2420
|
for (const nodes of this.scrollTimelines.values()) {
|
|
1898
2421
|
for (const node of nodes) {
|
|
1899
2422
|
if (this.topOf(node) === this.root)
|
|
@@ -1928,8 +2451,14 @@ export class Engine {
|
|
|
1928
2451
|
destroyNode(node) {
|
|
1929
2452
|
node.listeners?.clear();
|
|
1930
2453
|
node.listeners = null;
|
|
1931
|
-
node
|
|
2454
|
+
// With what it holds: a node Angular destroys can be put back, as content projected into
|
|
2455
|
+
// an `@if` is, and its children's views would then be handed to the view it is made again
|
|
2456
|
+
// as. Native gives a view one parent for its life, and aborts on a second.
|
|
2457
|
+
if (node.committed)
|
|
2458
|
+
this.forgetCommitted(node);
|
|
1932
2459
|
node.transitions = undefined;
|
|
2460
|
+
if (node.playing)
|
|
2461
|
+
this.stopNative(node, node.playing);
|
|
1933
2462
|
node.playing = undefined;
|
|
1934
2463
|
this.stopScrolled(node);
|
|
1935
2464
|
this.running.delete(node);
|
|
@@ -1950,6 +2479,9 @@ export class Engine {
|
|
|
1950
2479
|
return siblings[siblings.indexOf(node) + 1] ?? null;
|
|
1951
2480
|
}
|
|
1952
2481
|
setProp(node, key, value) {
|
|
2482
|
+
// A prop written again is a binding's, and is committed as any other is.
|
|
2483
|
+
if (node.attributeOnly?.delete(key))
|
|
2484
|
+
this.markProps(node, false);
|
|
1953
2485
|
if (value === undefined || value === null) {
|
|
1954
2486
|
// Nothing to remove, so nothing changed. Worth its own branch because it is the common
|
|
1955
2487
|
// case, not a rare one: a host primitive carries a host binding for every prop React Native
|
|
@@ -1967,7 +2499,32 @@ export class Engine {
|
|
|
1967
2499
|
// Inline style is applied after the cascade and inherited by nothing, so it cannot change
|
|
1968
2500
|
// what any node matches or inherits. Every other prop can: `[disabled]` is a selector. An
|
|
1969
2501
|
// inline `direction` is the exception, because the paragraphs below it align by it.
|
|
1970
|
-
this
|
|
2502
|
+
// And a child that takes one of this node's own values, an `inherit`, follows it.
|
|
2503
|
+
const heirs = node.styleCache?.heirs === true;
|
|
2504
|
+
this.markProps(node, key !== 'style' || this.inlineReachesStyle(node) || heirs);
|
|
2505
|
+
}
|
|
2506
|
+
/**
|
|
2507
|
+
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
2508
|
+
* `background-color` and `background-image` in the cascade, where a stylesheet wrote them, and
|
|
2509
|
+
* the child is the view that draws them. What a view that masks its children and not itself
|
|
2510
|
+
* needs, as `<gradient-text>` is.
|
|
2511
|
+
*/
|
|
2512
|
+
paintOn(node, child) {
|
|
2513
|
+
node.paintsOn = child;
|
|
2514
|
+
this.markProps(node, false);
|
|
2515
|
+
this.markProps(child, false);
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Keep a prop the node already has as an attribute: on the node for a selector such as
|
|
2519
|
+
* `:host([variant='primary'])` to match, and left out of what is committed to the native view.
|
|
2520
|
+
* It is committed again once the prop is next written. What the platform adapter does with a
|
|
2521
|
+
* static attribute that is an input of the host's component.
|
|
2522
|
+
*/
|
|
2523
|
+
keepAsAttribute(node, key) {
|
|
2524
|
+
if (!(key in node.props))
|
|
2525
|
+
return;
|
|
2526
|
+
(node.attributeOnly ??= new Set()).add(key);
|
|
2527
|
+
this.markProps(node, false);
|
|
1971
2528
|
}
|
|
1972
2529
|
/**
|
|
1973
2530
|
* Whether a node's inline style can change what it or anything below it resolves to: it sets,
|
|
@@ -2042,10 +2599,21 @@ export class Engine {
|
|
|
2042
2599
|
this.markProps(node);
|
|
2043
2600
|
}
|
|
2044
2601
|
setText(node, value) {
|
|
2602
|
+
const filled = (node.text === '') !== (value === '');
|
|
2045
2603
|
node.text = value;
|
|
2046
2604
|
this.markProps(node);
|
|
2047
|
-
if (node.parent)
|
|
2048
|
-
|
|
2605
|
+
if (!node.parent)
|
|
2606
|
+
return;
|
|
2607
|
+
this.markTextContent(node.parent, node);
|
|
2608
|
+
// Text of no length is nothing to `:empty`: its first character and its last change what
|
|
2609
|
+
// the element it is in matches, and what comes after that element.
|
|
2610
|
+
if (filled && this.structuralSheets)
|
|
2611
|
+
this.markFilled(node.parent);
|
|
2612
|
+
}
|
|
2613
|
+
markFilled(node) {
|
|
2614
|
+
node.styleDirty = true;
|
|
2615
|
+
this.markLaterSiblings(node);
|
|
2616
|
+
this.markPath(node);
|
|
2049
2617
|
}
|
|
2050
2618
|
setEventListener(node, topLevelType, fn) {
|
|
2051
2619
|
this.registerEventHandler();
|
|
@@ -2072,11 +2640,35 @@ export class Engine {
|
|
|
2072
2640
|
// state, though nothing about the sibling moved. Only sheets that ask about siblings pay.
|
|
2073
2641
|
if (this.structuralSheets)
|
|
2074
2642
|
this.markLaterSiblings(node);
|
|
2643
|
+
if (this.hasSheets)
|
|
2644
|
+
this.markBeneath(node.parent);
|
|
2075
2645
|
}
|
|
2076
2646
|
// The root has no parent to carry the mark, so it carries its own: `.dark` on the root alone
|
|
2077
2647
|
// restyles everything beneath it.
|
|
2078
2648
|
this.markPath(node.parent ?? node);
|
|
2079
2649
|
}
|
|
2650
|
+
/** Start matching ancestors again on a change, once a sheet that uses `:has()` is in play. */
|
|
2651
|
+
watchHas(sheet) {
|
|
2652
|
+
if (!sheet?.has || this.hasSheets)
|
|
2653
|
+
return;
|
|
2654
|
+
this.hasSheets = true;
|
|
2655
|
+
this.styles.tracksHas = true;
|
|
2656
|
+
// What is already resolved kept no record of the rules it matched: resolve it again. Nothing
|
|
2657
|
+
// is, when the sheet is the one the engine was made with.
|
|
2658
|
+
if (!this.root)
|
|
2659
|
+
return;
|
|
2660
|
+
this.root.styleDirty = true;
|
|
2661
|
+
this.markPath(this.root);
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Something at or beneath `from` changed: every node from there up may match a `:has()` rule
|
|
2665
|
+
* it did not, or stop matching one. They are matched again rather than marked for restyling,
|
|
2666
|
+
* which would restyle everything under them. Only sheets that use `:has()` pay.
|
|
2667
|
+
*/
|
|
2668
|
+
markBeneath(from) {
|
|
2669
|
+
for (let node = from; node; node = node.parent)
|
|
2670
|
+
node.hasDirty = true;
|
|
2671
|
+
}
|
|
2080
2672
|
markLaterSiblings(node) {
|
|
2081
2673
|
const siblings = node.parent?.children;
|
|
2082
2674
|
if (!siblings)
|
|
@@ -2085,22 +2677,101 @@ export class Engine {
|
|
|
2085
2677
|
siblings[i].styleDirty = true;
|
|
2086
2678
|
}
|
|
2087
2679
|
}
|
|
2088
|
-
/** `node`'s child list changed: `moved` came into it or went out of it. */
|
|
2089
|
-
markStructure(node, moved) {
|
|
2680
|
+
/** `node`'s child list changed: `moved` came into it at `at`, or went out of it from there. */
|
|
2681
|
+
markStructure(node, moved, at) {
|
|
2090
2682
|
node.structureDirty = true;
|
|
2091
2683
|
// A child list that moved changes what its members match, though nothing about them did:
|
|
2092
2684
|
// the old last row is no longer the last. Only sheets that ask about position pay for this.
|
|
2093
2685
|
if (this.structuralSheets) {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
//
|
|
2098
|
-
//
|
|
2099
|
-
|
|
2686
|
+
moved.styleDirty = true;
|
|
2687
|
+
this.markRepositioned(node.children, at, moved);
|
|
2688
|
+
// What the node itself matches hangs on its child list only through `:empty`, which only
|
|
2689
|
+
// its first child arriving or its last one leaving changes: and then what a later sibling
|
|
2690
|
+
// matches can too, `.box:empty + .spacer`. Styled again for any other change, everything
|
|
2691
|
+
// under it would be resolved again with it.
|
|
2692
|
+
if (wasOrIsEmptyWithout(node, moved)) {
|
|
2693
|
+
node.styleDirty = true;
|
|
2100
2694
|
this.markLaterSiblings(node);
|
|
2695
|
+
}
|
|
2101
2696
|
}
|
|
2697
|
+
if (this.hasSheets)
|
|
2698
|
+
this.markBeneath(node);
|
|
2102
2699
|
this.markPath(node.parent);
|
|
2103
2700
|
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Mark the children whose place in a child list that changed at `at` a sheet could read
|
|
2703
|
+
* differently. The two at each end always, which are the old first and last and the new. Those
|
|
2704
|
+
* from `at` on where some sheet counts from the start or asks about any one before, since a
|
|
2705
|
+
* child's place from the start hangs only on those before it; and those before `at` where one
|
|
2706
|
+
* counts from the end. Styling one again resolves everything under it again, so a long list
|
|
2707
|
+
* gaining a row at its end is not the whole list.
|
|
2708
|
+
*
|
|
2709
|
+
* Where the sheets ask no further than the element just before (`+`), a rule steps a known
|
|
2710
|
+
* number of elements along from one whose own match changed, and those are all there is to
|
|
2711
|
+
* mark: that many after the change, and that many after the two at the start, either of which
|
|
2712
|
+
* may have stopped or started being first. Nothing comes after the two at the end.
|
|
2713
|
+
*/
|
|
2714
|
+
markRepositioned(children, at, moved) {
|
|
2715
|
+
const steps = this.structuralAfter;
|
|
2716
|
+
const pastFirst = steps === Infinity ? 0 : steps;
|
|
2717
|
+
for (const child of elementsAtEnd(children, 2 + pastFirst, false))
|
|
2718
|
+
this.markPlace(child);
|
|
2719
|
+
for (const child of elementsAtEnd(children, 2, true))
|
|
2720
|
+
this.markPlace(child);
|
|
2721
|
+
if (this.structuralBefore)
|
|
2722
|
+
for (let i = 0; i < at; i++)
|
|
2723
|
+
this.markPlace(children[i]);
|
|
2724
|
+
// The elements after the change, not counting the one that came, which is marked already.
|
|
2725
|
+
let left = steps;
|
|
2726
|
+
for (let i = at; i < children.length && left > 0; i++) {
|
|
2727
|
+
const child = children[i];
|
|
2728
|
+
this.markPlace(child);
|
|
2729
|
+
if (child.kind === 'element' && child !== moved)
|
|
2730
|
+
left--;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* A child's place in its list changed, which is all that changed about it. It is matched
|
|
2735
|
+
* again, and keeps its style and all that is under it unless it now matches other rules: its
|
|
2736
|
+
* place is read by the rules that style it and by no other. Not where a rule reads the place
|
|
2737
|
+
* of an element like it from under it, `.row:nth-child(odd) .label`: that changes more than
|
|
2738
|
+
* the element itself, which is then styled again with everything under it, as is a child
|
|
2739
|
+
* that is no element.
|
|
2740
|
+
*/
|
|
2741
|
+
markPlace(child) {
|
|
2742
|
+
if (child.kind !== 'element' || this.placeReadFromUnder(child))
|
|
2743
|
+
child.styleDirty = true;
|
|
2744
|
+
else
|
|
2745
|
+
child.stateDirty = true;
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Whether a rule could read this element's place from another element. By its classes alone,
|
|
2749
|
+
* which every such compound it could be asks for: a compound with none could be any element.
|
|
2750
|
+
*/
|
|
2751
|
+
placeReadFromUnder(node) {
|
|
2752
|
+
for (const compound of this.placeElsewhere) {
|
|
2753
|
+
if (compound.classes.every((name) => node.classes?.has(name)))
|
|
2754
|
+
return true;
|
|
2755
|
+
}
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
/** Note what a sheet coming into play asks about a child list. None is unset: see `structural`. */
|
|
2759
|
+
watchStructure(sheet) {
|
|
2760
|
+
if (!sheet?.structural || this.watchedStructure.has(sheet))
|
|
2761
|
+
return;
|
|
2762
|
+
this.watchedStructure.add(sheet);
|
|
2763
|
+
this.structuralSheets = true;
|
|
2764
|
+
const reach = siblingReach(sheet);
|
|
2765
|
+
this.structuralAfter = Math.max(this.structuralAfter, reach.after);
|
|
2766
|
+
if (reach.before)
|
|
2767
|
+
this.structuralBefore = true;
|
|
2768
|
+
for (const compound of placeReadElsewhere(sheet))
|
|
2769
|
+
this.placeElsewhere.add(compound);
|
|
2770
|
+
// What a child whose place changed is compared with is the rules it matched, which a cache
|
|
2771
|
+
// keeps from here on. One made before has none, and is styled again the first time, as it
|
|
2772
|
+
// was before any sheet asked.
|
|
2773
|
+
this.styles.tracksHas = true;
|
|
2774
|
+
}
|
|
2104
2775
|
/** Walk up until we hit a node already known to have a dirty subtree. */
|
|
2105
2776
|
markPath(node) {
|
|
2106
2777
|
const wasClean = !this.root.structureDirty && !this.root.subtreeDirty;
|
|
@@ -2146,6 +2817,7 @@ export class Engine {
|
|
|
2146
2817
|
this.fabric.appendChildToSet(set, this.reconcileUnder(this.root, child));
|
|
2147
2818
|
}
|
|
2148
2819
|
this.clearFlags(this.root);
|
|
2820
|
+
this.announceCommit();
|
|
2149
2821
|
this.fabric.completeRoot(this.rootTag, set);
|
|
2150
2822
|
const elapsed = now() - started;
|
|
2151
2823
|
this.stats.commits++;
|
|
@@ -2160,6 +2832,7 @@ export class Engine {
|
|
|
2160
2832
|
this.stats.slowCommits++;
|
|
2161
2833
|
}
|
|
2162
2834
|
this.flushTransitionEvents();
|
|
2835
|
+
this.playSized();
|
|
2163
2836
|
this.scrollDriver?.afterCommit();
|
|
2164
2837
|
// Read first: the commit that settles keyframes starts by clearing it.
|
|
2165
2838
|
const facesAdded = this.facesAdded;
|
|
@@ -2169,6 +2842,25 @@ export class Engine {
|
|
|
2169
2842
|
this.rematchFonts();
|
|
2170
2843
|
return true;
|
|
2171
2844
|
}
|
|
2845
|
+
beforeCommit = null;
|
|
2846
|
+
announceCommit() {
|
|
2847
|
+
const run = this.beforeCommit;
|
|
2848
|
+
this.beforeCommit = null;
|
|
2849
|
+
run?.();
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Run `run` once, as the next commit hands its tree to the host and before the host has it: the
|
|
2853
|
+
* moment for something the host applies to its next commit, which is only this one from here.
|
|
2854
|
+
* A pass that changes nothing commits nothing and runs nothing, so a request waits until it is
|
|
2855
|
+
* taken back with the function returned. A later request replaces an earlier one.
|
|
2856
|
+
*/
|
|
2857
|
+
beforeNextCommit(run) {
|
|
2858
|
+
this.beforeCommit = run;
|
|
2859
|
+
return () => {
|
|
2860
|
+
if (this.beforeCommit === run)
|
|
2861
|
+
this.beforeCommit = null;
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2172
2864
|
/** The first commit whose new views have not been drawn yet, while `commitUnseen` runs. */
|
|
2173
2865
|
unseen = Infinity;
|
|
2174
2866
|
/**
|
|
@@ -2273,11 +2965,36 @@ export class Engine {
|
|
|
2273
2965
|
if (this.transitionEvents.length === 0)
|
|
2274
2966
|
return;
|
|
2275
2967
|
for (const { node, type, property } of this.transitionEvents.splice(0)) {
|
|
2968
|
+
// One listener that throws stops neither the rest nor the node being let go.
|
|
2276
2969
|
for (const listener of [...(node.listeners?.get(type) ?? [])]) {
|
|
2277
|
-
|
|
2970
|
+
try {
|
|
2971
|
+
listener(animationEvent(type, node, property));
|
|
2972
|
+
}
|
|
2973
|
+
catch (error) {
|
|
2974
|
+
this.reportEventError(error, type);
|
|
2975
|
+
}
|
|
2278
2976
|
}
|
|
2977
|
+
if (type === 'topAnimationend')
|
|
2978
|
+
this.letGo(node);
|
|
2279
2979
|
}
|
|
2280
2980
|
}
|
|
2981
|
+
/**
|
|
2982
|
+
* The nodes whose animation has ended with no fill and is still held at its last frame, until
|
|
2983
|
+
* its `animationend` has been heard. A browser runs that listener before it paints again, and
|
|
2984
|
+
* an exit animation's listener takes the element away: it is never seen back at rest. Here the
|
|
2985
|
+
* listener runs after the commit, so the commit that ends the animation paints the last frame,
|
|
2986
|
+
* and the element goes back to rest once the listener has run and left it there.
|
|
2987
|
+
*/
|
|
2988
|
+
lettingGo = new Set();
|
|
2989
|
+
letGo(node) {
|
|
2990
|
+
if (!this.lettingGo.delete(node))
|
|
2991
|
+
return;
|
|
2992
|
+
const running = node.playing;
|
|
2993
|
+
if (!running?.done)
|
|
2994
|
+
return;
|
|
2995
|
+
running.values = {};
|
|
2996
|
+
this.markProps(node, false);
|
|
2997
|
+
}
|
|
2281
2998
|
/** Anchors take part in sibling ordering but never reach Fabric. */
|
|
2282
2999
|
visibleChildren(node) {
|
|
2283
3000
|
// A view that takes its text as a prop has no text children to commit. See `ViewNameOptions`.
|
|
@@ -2287,13 +3004,21 @@ export class Engine {
|
|
|
2287
3004
|
/**
|
|
2288
3005
|
* Everything a node renders with, unprocessed. Precedence, weakest first: native defaults, the
|
|
2289
3006
|
* node's `defaultStyle`, matched CSS, explicit props, inline style, a component's
|
|
2290
|
-
* `styleOverride`. Inline wins over CSS
|
|
3007
|
+
* `styleOverride`. Inline wins over CSS as it does on the web, but for a declaration marked
|
|
3008
|
+
* `!important`, which stands over it: see `inlineOf`.
|
|
2291
3009
|
*
|
|
2292
3010
|
* Raw, because this is what the next commit diffs against. Colours and asset ids are converted
|
|
2293
3011
|
* on the way out instead (`processed`), and only for the keys that changed: the converters
|
|
2294
3012
|
* return fresh objects, so diffing their output would re-send every image and shadow on every
|
|
2295
3013
|
* unrelated change.
|
|
2296
3014
|
*/
|
|
3015
|
+
/** What a touch on an element that keeps a drag holds off, while it lasts: see `holdSwipe`. */
|
|
3016
|
+
heldFor(node, merged) {
|
|
3017
|
+
if (node === this.swipeHeld)
|
|
3018
|
+
merged['gestureResponseDistance'] = NO_SWIPE;
|
|
3019
|
+
if (node === this.scrollHeld)
|
|
3020
|
+
merged['scrollEnabled'] = false;
|
|
3021
|
+
}
|
|
2297
3022
|
mergeProps(node, viewName) {
|
|
2298
3023
|
if (node.kind === 'text')
|
|
2299
3024
|
return { text: paragraphText(node) };
|
|
@@ -2304,11 +3029,18 @@ export class Engine {
|
|
|
2304
3029
|
const props = { ...DEFAULT_PROPS[viewName], ...node.defaultStyle };
|
|
2305
3030
|
Object.assign(props, this.styles.resolve(node, this.styleEpoch).style);
|
|
2306
3031
|
const resolved = props['pointerEvents'];
|
|
2307
|
-
writeOwnProps(node.props, props);
|
|
3032
|
+
writeOwnProps(node.props, props, node.attributeOnly);
|
|
2308
3033
|
withTextContent(node, viewName, props);
|
|
2309
3034
|
const cascaded = props['transform'];
|
|
2310
|
-
const
|
|
3035
|
+
const inline = inlineOf(node);
|
|
3036
|
+
const style = boundTransform(node, Object.assign(props, inline), cascaded);
|
|
3037
|
+
if (inline !== NO_STYLE)
|
|
3038
|
+
this.styles.overOtherForms(inline, style);
|
|
2311
3039
|
nativePointerEvents(node, style, resolved);
|
|
3040
|
+
// Before an image's own size: `fit-content` is no size, so the image's is what it gets.
|
|
3041
|
+
fitContent(node, style);
|
|
3042
|
+
percentHeight(node, style);
|
|
3043
|
+
placeholderFaded(style);
|
|
2312
3044
|
const intrinsic = node.props[INTRINSIC_SIZE];
|
|
2313
3045
|
if (intrinsic)
|
|
2314
3046
|
applyIntrinsicSize(style, intrinsic);
|
|
@@ -2320,10 +3052,65 @@ export class Engine {
|
|
|
2320
3052
|
if (this.fontsRefreshed)
|
|
2321
3053
|
this.capForFonts(node, style);
|
|
2322
3054
|
alignMultiline(viewName, style);
|
|
3055
|
+
rowsTall(style, this.fontScale);
|
|
2323
3056
|
const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
2324
3057
|
centreSingleLine(viewName, merged, this.fontScale);
|
|
3058
|
+
// After a transition, which eases the basis as the basis it was written as.
|
|
3059
|
+
basisAsSize(node, merged);
|
|
3060
|
+
// After the basis, which is a size given where it is committed as one.
|
|
3061
|
+
ratioForAutoSize(merged);
|
|
3062
|
+
noOutline(merged);
|
|
3063
|
+
// Last, on what is committed: an override or an animation can hide a box, or place it.
|
|
3064
|
+
hiddenOutOfFlow(merged);
|
|
3065
|
+
delete merged['touchAction'];
|
|
3066
|
+
if (this.responders.has(node))
|
|
3067
|
+
stillTouched(merged);
|
|
3068
|
+
this.heldFor(node, merged);
|
|
3069
|
+
this.movePaint(node, merged);
|
|
2325
3070
|
return merged;
|
|
2326
3071
|
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Leave a node's background out of what it commits when a child paints it, and give that child
|
|
3074
|
+
* its parent's: the cascade's, and over it the parent's inline style.
|
|
3075
|
+
*/
|
|
3076
|
+
movePaint(node, merged) {
|
|
3077
|
+
if (node.paintsOn)
|
|
3078
|
+
for (const key of PAINT_KEYS)
|
|
3079
|
+
delete merged[key];
|
|
3080
|
+
const from = node.parent?.paintsOn === node ? node.parent : null;
|
|
3081
|
+
if (!from)
|
|
3082
|
+
return;
|
|
3083
|
+
const paint = { ...this.styles.resolve(from, this.styleEpoch).style, ...inlineOf(from) };
|
|
3084
|
+
for (const key of PAINT_KEYS)
|
|
3085
|
+
if (paint[key] !== undefined)
|
|
3086
|
+
merged[key] = paint[key];
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* A container whose direction or alignment changed since a child with a `fit-content` size
|
|
3090
|
+
* read it, or whose height stopped or started being one a percentage is taken of since a child
|
|
3091
|
+
* with a percentage height asked: those children are merged again. A change to a container
|
|
3092
|
+
* marks nothing beneath it, since none of these is inherited.
|
|
3093
|
+
*/
|
|
3094
|
+
refitChildren(node) {
|
|
3095
|
+
// Both are asked, and both remembered, before either has the children merged again.
|
|
3096
|
+
const basis = node.heightBasis;
|
|
3097
|
+
const based = basis !== undefined && definiteHeight(node) !== basis;
|
|
3098
|
+
const fitted = node.fitContainer !== undefined && containerOf(node) !== node.fitContainer;
|
|
3099
|
+
if (fitted)
|
|
3100
|
+
node.fitContainer = containerOf(node);
|
|
3101
|
+
if (based || fitted)
|
|
3102
|
+
this.refit(node);
|
|
3103
|
+
}
|
|
3104
|
+
/** Have a container's children merged again, through any `display: contents` among them. */
|
|
3105
|
+
refit(container) {
|
|
3106
|
+
for (const child of container.children) {
|
|
3107
|
+
if (child.kind !== 'element')
|
|
3108
|
+
continue;
|
|
3109
|
+
child.propsDirty = true;
|
|
3110
|
+
if (containerOf(child) === 'contents')
|
|
3111
|
+
this.refit(child);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
2327
3114
|
/**
|
|
2328
3115
|
* The text size cap that lays a paragraph or text input out again once a face it names has
|
|
2329
3116
|
* registered: a step per face above the app's own cap where it set one (below 1 is none, on
|
|
@@ -2352,7 +3139,7 @@ export class Engine {
|
|
|
2352
3139
|
if (!this.inlineDirection)
|
|
2353
3140
|
return own;
|
|
2354
3141
|
for (let at = node; at; at = at.parent) {
|
|
2355
|
-
const inline = textDirection(
|
|
3142
|
+
const inline = textDirection(inlineOf(at)['direction']);
|
|
2356
3143
|
if (inline)
|
|
2357
3144
|
return inline;
|
|
2358
3145
|
const cascaded = textDirection(at.styleCache?.style['direction']);
|
|
@@ -2380,6 +3167,7 @@ export class Engine {
|
|
|
2380
3167
|
if (!sheet || this.knownSheets.has(sheet))
|
|
2381
3168
|
return;
|
|
2382
3169
|
this.knownSheets.add(sheet);
|
|
3170
|
+
this.styles.noteSheet(sheet);
|
|
2383
3171
|
this.sheetOrder.push(sheet);
|
|
2384
3172
|
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
2385
3173
|
this.keyframes.set(name, sheet.keyframes[name]);
|
|
@@ -2397,6 +3185,8 @@ export class Engine {
|
|
|
2397
3185
|
sheetReplaced(sheet, next) {
|
|
2398
3186
|
if (sheet === next && this.knownSheets.has(sheet))
|
|
2399
3187
|
return;
|
|
3188
|
+
this.unwatchActive(sheet);
|
|
3189
|
+
this.watchActive(next);
|
|
2400
3190
|
const at = this.sheetOrder.indexOf(sheet);
|
|
2401
3191
|
if (at === -1) {
|
|
2402
3192
|
this.registerSheet(next);
|
|
@@ -2405,6 +3195,7 @@ export class Engine {
|
|
|
2405
3195
|
this.knownSheets.delete(sheet);
|
|
2406
3196
|
if (next && !this.knownSheets.has(next)) {
|
|
2407
3197
|
this.knownSheets.add(next);
|
|
3198
|
+
this.styles.noteSheet(next);
|
|
2408
3199
|
this.sheetOrder[at] = next;
|
|
2409
3200
|
}
|
|
2410
3201
|
else {
|
|
@@ -2459,11 +3250,17 @@ export class Engine {
|
|
|
2459
3250
|
}
|
|
2460
3251
|
/** Mark every element under `node` committed with a family, so it is matched again. */
|
|
2461
3252
|
markFontText(node) {
|
|
3253
|
+
const named = (text) => {
|
|
3254
|
+
if (typeof text?.committed?.props['fontFamily'] === 'string')
|
|
3255
|
+
this.markProps(text, false);
|
|
3256
|
+
};
|
|
2462
3257
|
for (const child of node.children) {
|
|
3258
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
3259
|
+
if (child.kind === 'text')
|
|
3260
|
+
named(child.box);
|
|
2463
3261
|
if (child.kind !== 'element')
|
|
2464
3262
|
continue;
|
|
2465
|
-
|
|
2466
|
-
this.markProps(child, false);
|
|
3263
|
+
named(child);
|
|
2467
3264
|
this.markFontText(child);
|
|
2468
3265
|
}
|
|
2469
3266
|
}
|
|
@@ -2510,6 +3307,7 @@ export class Engine {
|
|
|
2510
3307
|
handOverToScroll(node, spec) {
|
|
2511
3308
|
if (!node.playing)
|
|
2512
3309
|
return;
|
|
3310
|
+
this.stopNative(node, node.playing);
|
|
2513
3311
|
if (node.playing.spec.name !== spec.name)
|
|
2514
3312
|
this.cancelPlaying(node);
|
|
2515
3313
|
node.playing = undefined;
|
|
@@ -2521,9 +3319,146 @@ export class Engine {
|
|
|
2521
3319
|
*/
|
|
2522
3320
|
cancelPlaying(node) {
|
|
2523
3321
|
const running = node.playing;
|
|
3322
|
+
if (running)
|
|
3323
|
+
this.stopNative(node, running);
|
|
2524
3324
|
if (running && !running.done)
|
|
2525
3325
|
this.emitTransition(node, 'topAnimationcancel', running.spec.name);
|
|
2526
3326
|
}
|
|
3327
|
+
/** Stop on native each animation whose view has left the tree. */
|
|
3328
|
+
releasePlayedNatively() {
|
|
3329
|
+
for (const node of [...this.playedNatively]) {
|
|
3330
|
+
if (this.topOf(node) === this.root)
|
|
3331
|
+
continue;
|
|
3332
|
+
this.stopNative(node, node.playing);
|
|
3333
|
+
node.playing = undefined;
|
|
3334
|
+
this.markProps(node, false);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
/** Every node whose animation native is playing, with no frame of it in JavaScript. */
|
|
3338
|
+
playedNatively = new Set();
|
|
3339
|
+
/**
|
|
3340
|
+
* Hand an animation that has just started to native, where native can play it: one that moves
|
|
3341
|
+
* only opacity and transforms, starts at once, and repeats a whole number of times. Native then
|
|
3342
|
+
* runs it frame by frame, and JavaScript hears of it again when it ends. Anything else is
|
|
3343
|
+
* played from here, a commit a frame. See `NativeScrollDriver.play`.
|
|
3344
|
+
*/
|
|
3345
|
+
/**
|
|
3346
|
+
* The nodes whose animation moves them by a share of their own size and had no size yet when
|
|
3347
|
+
* it started, each with the style it rests at: asked again once the commit has laid them out.
|
|
3348
|
+
*/
|
|
3349
|
+
unsized = new Map();
|
|
3350
|
+
/**
|
|
3351
|
+
* An animation's frames as native can interpolate them: a move by a share of the box,
|
|
3352
|
+
* `translateX(200%)`, as the points that is of the size the view is laid out at. Nothing where
|
|
3353
|
+
* the view has no size yet, which is every view until its first commit: it is played from
|
|
3354
|
+
* JavaScript, and handed over once that commit has laid it out. See `playSized`.
|
|
3355
|
+
*
|
|
3356
|
+
* ponytail: the size it had when it started. A box that changes size goes on moving by the
|
|
3357
|
+
* points of the old one until the animation starts again; restart it from the box's layout
|
|
3358
|
+
* event if a bar that resizes mid-animation comes up.
|
|
3359
|
+
*/
|
|
3360
|
+
sized(node, running, props) {
|
|
3361
|
+
// One that waits, or takes no time, is played from here whatever it moves by.
|
|
3362
|
+
if (running.spec.delay !== 0 || running.spec.duration <= 0)
|
|
3363
|
+
return null;
|
|
3364
|
+
if (!movesByShare(running.tracks, props))
|
|
3365
|
+
return { tracks: running.tracks, resting: props };
|
|
3366
|
+
let size;
|
|
3367
|
+
this.measure(node, (frame) => (size = frame));
|
|
3368
|
+
if (size)
|
|
3369
|
+
return sharesAsPoints(running.tracks, props, size);
|
|
3370
|
+
this.unsized.set(node, props);
|
|
3371
|
+
return null;
|
|
3372
|
+
}
|
|
3373
|
+
/**
|
|
3374
|
+
* Hand to native each animation that was waiting for its view to have a size. The commit just
|
|
3375
|
+
* made painted its first frame, so its clock starts here: no frame of it has been seen move.
|
|
3376
|
+
*/
|
|
3377
|
+
playSized() {
|
|
3378
|
+
if (!this.unsized.size)
|
|
3379
|
+
return;
|
|
3380
|
+
const waiting = [...this.unsized];
|
|
3381
|
+
this.unsized.clear();
|
|
3382
|
+
for (const [node, props] of waiting) {
|
|
3383
|
+
const running = node.playing;
|
|
3384
|
+
if (!running || running.done || running.native || !this.playing.has(node))
|
|
3385
|
+
continue;
|
|
3386
|
+
// Native plays it from its first frame, so its clock starts here: but only where native
|
|
3387
|
+
// takes it. Left in JavaScript, it goes on from the clock it started on.
|
|
3388
|
+
const started = running.start;
|
|
3389
|
+
running.start = this.now();
|
|
3390
|
+
if (this.playNatively(node, running, props)) {
|
|
3391
|
+
this.playing.delete(node);
|
|
3392
|
+
// `collapsable`, which keeps a view for native to move, is in what it holds now.
|
|
3393
|
+
this.markProps(node, false);
|
|
3394
|
+
}
|
|
3395
|
+
else {
|
|
3396
|
+
running.start = started;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
// One with no size after its commit is not laid out at all, and is played from JavaScript.
|
|
3400
|
+
this.unsized.clear();
|
|
3401
|
+
}
|
|
3402
|
+
playNatively(node, running, props) {
|
|
3403
|
+
const { spec } = running;
|
|
3404
|
+
const sized = this.scrollDriver ? this.sized(node, running, props) : null;
|
|
3405
|
+
if (!this.scrollDriver || !sized)
|
|
3406
|
+
return false;
|
|
3407
|
+
const { channels, held, span } = clockChannels(sized.tracks, spec, sized.resting);
|
|
3408
|
+
if (held.length || !(channels.opacity || channels.transform.length))
|
|
3409
|
+
return false;
|
|
3410
|
+
// There and back is one native animation: an odd number of ways has no whole number of them.
|
|
3411
|
+
const count = spec.iterations === null ? -1 : spec.iterations / span;
|
|
3412
|
+
if (count !== -1 && !(Number.isInteger(count) && count > 0))
|
|
3413
|
+
return false;
|
|
3414
|
+
const length = Math.max(2, Math.round((spec.duration * span) / NATIVE_FRAME) + 1);
|
|
3415
|
+
const frames = Array.from({ length }, (_, i) => i / (length - 1));
|
|
3416
|
+
const native = this.scrollDriver.play(node, channels, { frames, toValue: span, iterations: count }, (view) => this.tagOf(view), () => this.endedNatively(node, running));
|
|
3417
|
+
if (!native)
|
|
3418
|
+
return false;
|
|
3419
|
+
running.native = native;
|
|
3420
|
+
// As Animated does: Fabric flattens a view that only lays out, and then there is no native
|
|
3421
|
+
// view for the animation to move.
|
|
3422
|
+
running.values = { ...running.values, collapsable: false };
|
|
3423
|
+
this.playedNatively.add(node);
|
|
3424
|
+
return true;
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Native reached the end of an animation it was playing. The frame it ends at, or the resting
|
|
3428
|
+
* style where it does not fill, is committed before the view is let go, so nothing shows of
|
|
3429
|
+
* the frame it started at, which is what the view had been committed with.
|
|
3430
|
+
*/
|
|
3431
|
+
endedNatively(node, running) {
|
|
3432
|
+
if (node.playing !== running || !running.native)
|
|
3433
|
+
return;
|
|
3434
|
+
const { native, spec } = running;
|
|
3435
|
+
running.native = undefined;
|
|
3436
|
+
this.playedNatively.delete(node);
|
|
3437
|
+
const end = running.start + spec.duration * (spec.iterations ?? 1);
|
|
3438
|
+
this.advancePlayer(node, Math.max(this.now(), end));
|
|
3439
|
+
this.commit();
|
|
3440
|
+
native.stop();
|
|
3441
|
+
}
|
|
3442
|
+
/** Take an animation back from native, which stops it there. The caller plays it on, or not. */
|
|
3443
|
+
stopNative(node, running) {
|
|
3444
|
+
if (!running.native)
|
|
3445
|
+
return;
|
|
3446
|
+
running.native.stop();
|
|
3447
|
+
running.native = undefined;
|
|
3448
|
+
this.playedNatively.delete(node);
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* Play on from JavaScript an animation native was playing, from where its clock has got to:
|
|
3452
|
+
* what a pause, other frames or a view made again need, none of which native can be told of.
|
|
3453
|
+
*/
|
|
3454
|
+
backToScript(node, running) {
|
|
3455
|
+
if (!running.native)
|
|
3456
|
+
return;
|
|
3457
|
+
this.stopNative(node, running);
|
|
3458
|
+
running.values = sample(running, this.now()).values;
|
|
3459
|
+
if (!running.done && running.pausedAt === undefined)
|
|
3460
|
+
this.playing.add(node);
|
|
3461
|
+
}
|
|
2527
3462
|
/** Start the clock on an animation, unless the node is already playing this one. */
|
|
2528
3463
|
startPlaying(node, spec, frames, props) {
|
|
2529
3464
|
const current = node.playing;
|
|
@@ -2531,6 +3466,8 @@ export class Engine {
|
|
|
2531
3466
|
if (!current || !sameAnimation(current.spec, spec)) {
|
|
2532
3467
|
if (current && current.spec.name !== spec.name)
|
|
2533
3468
|
this.cancelPlaying(node);
|
|
3469
|
+
else if (current)
|
|
3470
|
+
this.stopNative(node, current);
|
|
2534
3471
|
const started = {
|
|
2535
3472
|
spec,
|
|
2536
3473
|
tracks: tracksOf(frames, props, inherited),
|
|
@@ -2551,12 +3488,16 @@ export class Engine {
|
|
|
2551
3488
|
node.playing = started;
|
|
2552
3489
|
if (spec.paused)
|
|
2553
3490
|
started.pausedAt = this.now();
|
|
2554
|
-
else
|
|
3491
|
+
else if (!this.playNatively(node, started, props))
|
|
2555
3492
|
this.playing.add(node);
|
|
2556
3493
|
this.emitTransition(node, 'topAnimationstart', spec.name);
|
|
2557
3494
|
return;
|
|
2558
3495
|
}
|
|
3496
|
+
// A view made again is another view: the one native was moving is gone.
|
|
3497
|
+
if (node.committed === null)
|
|
3498
|
+
this.backToScript(node, current);
|
|
2559
3499
|
if (this.playedFrames.get(current) !== frames || inherited !== current.inherited) {
|
|
3500
|
+
this.backToScript(node, current);
|
|
2560
3501
|
this.reframe(node, current, frames, props, inherited);
|
|
2561
3502
|
}
|
|
2562
3503
|
this.playOrPause(node, node.playing, spec);
|
|
@@ -2584,6 +3525,10 @@ export class Engine {
|
|
|
2584
3525
|
reframed.values = finished && !holds ? {} : values;
|
|
2585
3526
|
this.playedFrames.set(reframed, frames);
|
|
2586
3527
|
node.playing = reframed;
|
|
3528
|
+
// One that had nothing to paint was let go of: with frames that say something it plays on.
|
|
3529
|
+
const stopped = reframed.done || reframed.pausedAt !== undefined || reframed.native;
|
|
3530
|
+
if (!stopped)
|
|
3531
|
+
this.playing.add(node);
|
|
2587
3532
|
}
|
|
2588
3533
|
/**
|
|
2589
3534
|
* `animation-play-state` changed on an animation already playing: pausing holds the frame it
|
|
@@ -2594,6 +3539,7 @@ export class Engine {
|
|
|
2594
3539
|
current.spec = spec;
|
|
2595
3540
|
const now = this.now();
|
|
2596
3541
|
if (spec.paused && current.pausedAt === undefined && !current.done) {
|
|
3542
|
+
this.backToScript(node, current);
|
|
2597
3543
|
current.pausedAt = now;
|
|
2598
3544
|
this.playing.delete(node);
|
|
2599
3545
|
}
|
|
@@ -2762,6 +3708,21 @@ export class Engine {
|
|
|
2762
3708
|
node.transitions = undefined;
|
|
2763
3709
|
return props;
|
|
2764
3710
|
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Whether an animation that has finished goes on holding its last frame. With a forwards fill
|
|
3713
|
+
* it does, for good: without one the element falls back to whatever the cascade gives it, and
|
|
3714
|
+
* 'backwards' fills the start only. Heard, it holds until whoever listens has been told: see
|
|
3715
|
+
* `lettingGo`.
|
|
3716
|
+
*/
|
|
3717
|
+
keeps(node, running) {
|
|
3718
|
+
if (running.spec.fill === 'forwards' || running.spec.fill === 'both')
|
|
3719
|
+
return true;
|
|
3720
|
+
// One that listened and has stopped leaves an empty set behind: nobody to hear it.
|
|
3721
|
+
if (!node.listeners?.get('topAnimationend')?.size)
|
|
3722
|
+
return false;
|
|
3723
|
+
this.lettingGo.add(node);
|
|
3724
|
+
return true;
|
|
3725
|
+
}
|
|
2765
3726
|
/** One `@keyframes` player, one frame on. */
|
|
2766
3727
|
advancePlayer(node, now) {
|
|
2767
3728
|
const running = node.playing;
|
|
@@ -2773,12 +3734,19 @@ export class Engine {
|
|
|
2773
3734
|
this.playing.delete(node);
|
|
2774
3735
|
return;
|
|
2775
3736
|
}
|
|
3737
|
+
// Frames that say nothing paint nothing: a keyframe's one declaration refused at build time
|
|
3738
|
+
// leaves these. No frame of it is committed, and one that never ends has nothing left to do.
|
|
3739
|
+
const idle = running.tracks.size === 0;
|
|
2776
3740
|
const { values, finished } = sample(running, now);
|
|
2777
|
-
//
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
3741
|
+
// One of no time is finished already, endless or not, and ends as any other does.
|
|
3742
|
+
if (idle && !finished && running.spec.iterations === null) {
|
|
3743
|
+
this.playing.delete(node);
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
running.values = finished && !this.keeps(node, running) ? {} : values;
|
|
3747
|
+
// Its end is still a commit: that is what its `animationend` is sent after.
|
|
3748
|
+
if (!idle || finished)
|
|
3749
|
+
this.markProps(node, false);
|
|
2782
3750
|
if (!finished)
|
|
2783
3751
|
return;
|
|
2784
3752
|
running.done = true;
|
|
@@ -2904,7 +3872,8 @@ export class Engine {
|
|
|
2904
3872
|
*
|
|
2905
3873
|
* A stop's colour is a colour like any other, but it is two levels down and nothing else looks
|
|
2906
3874
|
* there: without this the device is handed the string `rgb(255, 0, 0)` where it wants a number,
|
|
2907
|
-
* and paints nothing.
|
|
3875
|
+
* and paints nothing. The stops are first rewritten so native's interpolation paints what
|
|
3876
|
+
* CSS's premultiplied one does (see `premultipliedStops`).
|
|
2908
3877
|
*/
|
|
2909
3878
|
processGradients(value) {
|
|
2910
3879
|
if (!Array.isArray(value))
|
|
@@ -2915,7 +3884,7 @@ export class Engine {
|
|
|
2915
3884
|
return entry;
|
|
2916
3885
|
return {
|
|
2917
3886
|
...gradient,
|
|
2918
|
-
colorStops: gradient.colorStops.map((stop) => ({
|
|
3887
|
+
colorStops: premultipliedStops(gradient.colorStops).map((stop) => ({
|
|
2919
3888
|
...stop,
|
|
2920
3889
|
color: this.color(stop.color),
|
|
2921
3890
|
})),
|
|
@@ -2937,7 +3906,9 @@ export class Engine {
|
|
|
2937
3906
|
if (this.isClean(node) && !styleChanged)
|
|
2938
3907
|
return node.committed.handle;
|
|
2939
3908
|
node.styleCommitted = style;
|
|
2940
|
-
|
|
3909
|
+
repaint(node);
|
|
3910
|
+
this.refitChildren(node);
|
|
3911
|
+
const viewName = committedViewName(node);
|
|
2941
3912
|
this.forgetRenamed(node, viewName);
|
|
2942
3913
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
2943
3914
|
noteTouches(node, style);
|
|
@@ -3075,6 +4046,8 @@ export class Engine {
|
|
|
3075
4046
|
* when it is shown again, as Modal.js creates it afresh. Committed again, it ignores every touch.
|
|
3076
4047
|
*/
|
|
3077
4048
|
withheld(child) {
|
|
4049
|
+
if (this.undisplayed(child))
|
|
4050
|
+
return true;
|
|
3078
4051
|
if (child.props['visible'] !== false || child.presented)
|
|
3079
4052
|
return false;
|
|
3080
4053
|
if (viewNameOf(child) !== MODAL_HOST)
|
|
@@ -3083,6 +4056,42 @@ export class Engine {
|
|
|
3083
4056
|
this.forgetCommitted(child);
|
|
3084
4057
|
return true;
|
|
3085
4058
|
}
|
|
4059
|
+
/**
|
|
4060
|
+
* Whether an element is `display: none`, which has no box and so no view, nor has anything
|
|
4061
|
+
* in it. A view that is committed and not displayed looks the same, but Yoga marks one as
|
|
4062
|
+
* laid out each time it measures what holds it, and React Native clears the mark only where it
|
|
4063
|
+
* reads that parent's layout: a debug build stops at the mark, on a later commit that has the
|
|
4064
|
+
* parent's layout already. Shown again it is made again, as an element put back in the tree
|
|
4065
|
+
* is: what its views held of their own, a scroll offset, is not kept.
|
|
4066
|
+
*/
|
|
4067
|
+
undisplayed(child) {
|
|
4068
|
+
if (child.kind !== 'element')
|
|
4069
|
+
return false;
|
|
4070
|
+
this.styles.resolve(child, this.styleEpoch);
|
|
4071
|
+
if (ownLayout(child, 'display') !== 'none')
|
|
4072
|
+
return false;
|
|
4073
|
+
if (child.committed) {
|
|
4074
|
+
this.forgetCommitted(child);
|
|
4075
|
+
// A field in it has no view to send its blur from.
|
|
4076
|
+
if (isWithin(this.focusedNode, child))
|
|
4077
|
+
this.setFocused(null);
|
|
4078
|
+
}
|
|
4079
|
+
return true;
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Whether a node hoisted into `target` is not displayed where it was written: it, or a box
|
|
4083
|
+
* between it and the child of `target` it was written in. That child is asked before this is.
|
|
4084
|
+
*/
|
|
4085
|
+
undisplayedUnder(target, written) {
|
|
4086
|
+
for (let at = written; at && at.parent !== target; at = at.parent) {
|
|
4087
|
+
if (!this.undisplayed(at))
|
|
4088
|
+
continue;
|
|
4089
|
+
if (written.committed)
|
|
4090
|
+
this.forgetCommitted(written);
|
|
4091
|
+
return true;
|
|
4092
|
+
}
|
|
4093
|
+
return false;
|
|
4094
|
+
}
|
|
3086
4095
|
/**
|
|
3087
4096
|
* `reconcileChildren` for a node other nodes hoist into: its own children, each followed by
|
|
3088
4097
|
* the hoisted nodes written inside it, then those written in it directly, then the views it
|
|
@@ -3105,9 +4114,10 @@ export class Engine {
|
|
|
3105
4114
|
else
|
|
3106
4115
|
handles.push(this.reconcileUnder(node, child, context));
|
|
3107
4116
|
const moved = hoisted?.get(child);
|
|
3108
|
-
|
|
3109
|
-
|
|
4117
|
+
for (const written of moved ?? []) {
|
|
4118
|
+
if (!this.undisplayedUnder(node, written))
|
|
3110
4119
|
handles.push(this.land(node, written, landed));
|
|
4120
|
+
}
|
|
3111
4121
|
}
|
|
3112
4122
|
for (const child of direct)
|
|
3113
4123
|
handles.push(this.land(node, child, landed));
|
|
@@ -3276,7 +4286,10 @@ export class Engine {
|
|
|
3276
4286
|
* layout bug. On the first commit of any known element with no component behind it, say so.
|
|
3277
4287
|
*/
|
|
3278
4288
|
checkClaimed(node) {
|
|
3279
|
-
|
|
4289
|
+
// The paragraph the engine wraps loose text in is its own: no template wrote it.
|
|
4290
|
+
if (node.kind !== 'element' || node.claimed || node.anonymous)
|
|
4291
|
+
return;
|
|
4292
|
+
if (!PRIMITIVE_NAMES.has(node.name))
|
|
3280
4293
|
return;
|
|
3281
4294
|
if (this.reported.has(node.name))
|
|
3282
4295
|
return;
|
|
@@ -3393,9 +4406,12 @@ export class Engine {
|
|
|
3393
4406
|
this.registerEventHandler();
|
|
3394
4407
|
this.responders.set(node, handlers);
|
|
3395
4408
|
this.keepNative(node, true);
|
|
4409
|
+
// What it is committed with can hang on whether it takes a touch: see `stillTouched`.
|
|
4410
|
+
this.markProps(node);
|
|
3396
4411
|
return () => {
|
|
3397
4412
|
this.responders.delete(node);
|
|
3398
4413
|
this.keepNative(node, false);
|
|
4414
|
+
this.markProps(node);
|
|
3399
4415
|
if (this.currentResponder !== node)
|
|
3400
4416
|
return;
|
|
3401
4417
|
// Torn down under the finger: no release is coming, so the chain it marked is cleared
|
|
@@ -3627,7 +4643,9 @@ export class Engine {
|
|
|
3627
4643
|
const handle = node.committed?.handle;
|
|
3628
4644
|
if (!handle || !this.fabric.setIsJSResponder)
|
|
3629
4645
|
return;
|
|
3630
|
-
const
|
|
4646
|
+
const asked = this.responders.get(node)?.blockNativeResponder ?? false;
|
|
4647
|
+
// And for a drag an element's `touch-action` keeps from the page: see `holdSwipe`.
|
|
4648
|
+
const block = asked || this.drag?.held === true;
|
|
3631
4649
|
this.fabric.setIsJSResponder(handle, isResponder, block);
|
|
3632
4650
|
}
|
|
3633
4651
|
/** Runs before listener propagation, as React's responder plugin does. */
|
|
@@ -3689,6 +4707,10 @@ export class Engine {
|
|
|
3689
4707
|
dispatchEvent(instanceHandle, topLevelType, nativeEvent) {
|
|
3690
4708
|
const target = instanceHandle;
|
|
3691
4709
|
this.trace(target, topLevelType);
|
|
4710
|
+
// What has the focus does not take it again: native answering a focus it was told of, where
|
|
4711
|
+
// whoever asked has already said so.
|
|
4712
|
+
if (topLevelType === 'topFocus' && target !== null && this.focusedNode === target)
|
|
4713
|
+
return;
|
|
3692
4714
|
// Listeners get RN's documented shape, `{nativeEvent}`. Fabric hands us the payload bare;
|
|
3693
4715
|
// React wraps it in a synthetic event and every RN API is written against `event.nativeEvent`,
|
|
3694
4716
|
// so handlers ported from RN would silently read undefined otherwise.
|
|
@@ -3699,6 +4721,7 @@ export class Engine {
|
|
|
3699
4721
|
if (topLevelType === 'topScroll' && target && this.scrollTimelines.has(target)) {
|
|
3700
4722
|
this.measuredScroll(target, nativeEvent);
|
|
3701
4723
|
}
|
|
4724
|
+
this.holdSwipe(target, topLevelType, nativeEvent);
|
|
3702
4725
|
this.trackFocus(target, topLevelType);
|
|
3703
4726
|
if (target)
|
|
3704
4727
|
this.cancelPressForScroll(target, topLevelType, event);
|
|
@@ -3710,6 +4733,116 @@ export class Engine {
|
|
|
3710
4733
|
}
|
|
3711
4734
|
this.propagate(target, topLevelType, event);
|
|
3712
4735
|
}
|
|
4736
|
+
/** The screen whose swipe back is held off while a finger is on what keeps a sideways drag. */
|
|
4737
|
+
swipeHeld = null;
|
|
4738
|
+
/**
|
|
4739
|
+
* Hold a screen's swipe back off for a touch that starts on an element whose `touch-action`
|
|
4740
|
+
* keeps a drag to the side for itself, and give it back when the last finger lifts. From iOS
|
|
4741
|
+
* 26 the swipe starts anywhere on a screen once a finger has moved a little to the right, and
|
|
4742
|
+
* takes the touch from whatever was following it: a slider's thumb stops and the screen
|
|
4743
|
+
* leaves. The screen is told before the finger has moved that far.
|
|
4744
|
+
*/
|
|
4745
|
+
holdSwipe(target, type, nativeEvent) {
|
|
4746
|
+
if (type === TOUCH_START)
|
|
4747
|
+
this.holdFrom(target, nativeEvent);
|
|
4748
|
+
else if (type === 'topTouchMove')
|
|
4749
|
+
this.settleDrag(nativeEvent);
|
|
4750
|
+
else if (type === 'topTouchEnd' || type === 'topTouchCancel')
|
|
4751
|
+
this.releaseSwipe(nativeEvent);
|
|
4752
|
+
}
|
|
4753
|
+
/**
|
|
4754
|
+
* A touch on an element that keeps a drag to the side: where it began, and whether native's
|
|
4755
|
+
* own gestures are held off for it. Held from the start where the element keeps a drag down
|
|
4756
|
+
* the page too (`none`); from the moment the finger sets off across where it leaves that one
|
|
4757
|
+
* to the page (`pan-y`), and never for that touch where it sets off down.
|
|
4758
|
+
*/
|
|
4759
|
+
drag = null;
|
|
4760
|
+
holdFrom(target, nativeEvent) {
|
|
4761
|
+
// A first finger down is a new touch: the last one's end never came where what it was on
|
|
4762
|
+
// went from under the finger.
|
|
4763
|
+
const fingers = nativeEvent?.touches?.length;
|
|
4764
|
+
if (this.drag && fingers === 1)
|
|
4765
|
+
this.releaseSwipe(null);
|
|
4766
|
+
if (this.drag || !target)
|
|
4767
|
+
return;
|
|
4768
|
+
const actions = touchActions(target);
|
|
4769
|
+
if (!keeps(actions, PANS_ACROSS))
|
|
4770
|
+
return;
|
|
4771
|
+
const all = keeps(actions, PANS_ALONG);
|
|
4772
|
+
this.drag = { ...pointOf(nativeEvent), settled: all, held: all, on: target };
|
|
4773
|
+
if (all)
|
|
4774
|
+
this.holdScroll(target);
|
|
4775
|
+
const screen = this.screenOf(target);
|
|
4776
|
+
if (!screen)
|
|
4777
|
+
return;
|
|
4778
|
+
this.swipeHeld = screen;
|
|
4779
|
+
this.tellScreen(screen);
|
|
4780
|
+
}
|
|
4781
|
+
/** The scroll view whose scrolling is off while a drag over it is an element's own. */
|
|
4782
|
+
scrollHeld = null;
|
|
4783
|
+
/**
|
|
4784
|
+
* Stop the scroll view a node is in from scrolling, until the touch ends. Telling native who
|
|
4785
|
+
* holds the touch does this on Android. On iOS a scroll view is held off only by a responder
|
|
4786
|
+
* that is over it, never one inside it, so it is told not to scroll.
|
|
4787
|
+
*/
|
|
4788
|
+
holdScroll(node) {
|
|
4789
|
+
for (let up = node.parent; up; up = up.parent) {
|
|
4790
|
+
if (up.kind !== 'element' || !SCROLL_VIEWS.has(viewNameOf(up)))
|
|
4791
|
+
continue;
|
|
4792
|
+
this.scrollHeld = up;
|
|
4793
|
+
this.tellScreen(up);
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
/** Settle which way a drag set off, and hold native's own off one that set off across. */
|
|
4798
|
+
settleDrag(nativeEvent) {
|
|
4799
|
+
const drag = this.drag;
|
|
4800
|
+
if (!drag || drag.settled)
|
|
4801
|
+
return;
|
|
4802
|
+
const at = pointOf(nativeEvent);
|
|
4803
|
+
const [across, along] = [Math.abs(at.x - drag.x), Math.abs(at.y - drag.y)];
|
|
4804
|
+
if (Math.max(across, along) < DRAG_SLOP)
|
|
4805
|
+
return;
|
|
4806
|
+
drag.settled = true;
|
|
4807
|
+
drag.held = across > along;
|
|
4808
|
+
if (!drag.held)
|
|
4809
|
+
return;
|
|
4810
|
+
if (this.currentResponder)
|
|
4811
|
+
this.tellNative(this.currentResponder, true);
|
|
4812
|
+
this.holdScroll(drag.on);
|
|
4813
|
+
}
|
|
4814
|
+
/** Give the swipe back once the last finger has lifted. */
|
|
4815
|
+
releaseSwipe(nativeEvent) {
|
|
4816
|
+
const left = nativeEvent?.touches?.length ?? 0;
|
|
4817
|
+
if (left > 0)
|
|
4818
|
+
return;
|
|
4819
|
+
this.drag = null;
|
|
4820
|
+
for (const held of [this.scrollHeld, this.swipeHeld]) {
|
|
4821
|
+
if (held === this.scrollHeld)
|
|
4822
|
+
this.scrollHeld = null;
|
|
4823
|
+
else
|
|
4824
|
+
this.swipeHeld = null;
|
|
4825
|
+
if (held)
|
|
4826
|
+
this.tellScreen(held);
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
/**
|
|
4830
|
+
* Commit a screen or a scroll view again now, not with whatever next changes: the swipe begins
|
|
4831
|
+
* once the finger has moved ten points or so, which is a frame or two after it came down.
|
|
4832
|
+
*/
|
|
4833
|
+
tellScreen(screen) {
|
|
4834
|
+
// Its props alone: nothing a selector reads moved, so nothing under it is styled again.
|
|
4835
|
+
this.markProps(screen, false);
|
|
4836
|
+
this.commit();
|
|
4837
|
+
}
|
|
4838
|
+
/** The screen of a native stack a node is on, or nothing where it is on none. */
|
|
4839
|
+
screenOf(node) {
|
|
4840
|
+
for (let up = node; up; up = up.parent) {
|
|
4841
|
+
if (up.kind === 'element' && viewNameOf(up) === STACK_SCREEN)
|
|
4842
|
+
return up;
|
|
4843
|
+
}
|
|
4844
|
+
return null;
|
|
4845
|
+
}
|
|
3713
4846
|
/** A presented modal host has finished leaving, so a hidden one can come out of the tree. */
|
|
3714
4847
|
dismissed(target) {
|
|
3715
4848
|
if (!target.presented)
|