@ng-native/fabric 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css.d.ts +55 -1
- package/dist/css.js +250 -17
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +66 -0
- package/dist/engine.js +326 -32
- package/dist/engine.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -323,6 +323,10 @@ export interface EngineNode extends HostNode {
|
|
|
323
323
|
baselineFields?: Set<EngineNode>;
|
|
324
324
|
/** Commit this node again with its children so Fabric measures it again. `remeasureText`. */
|
|
325
325
|
remeasure?: true;
|
|
326
|
+
/** On a paragraph, whether its text was one word when it was last merged: `wholeWords`. */
|
|
327
|
+
oneWord?: boolean;
|
|
328
|
+
/** The z-index this view is drawn at for a box in it, where it has none: `Engine.raise`. */
|
|
329
|
+
raised?: number;
|
|
326
330
|
/**
|
|
327
331
|
* A modal host committed as visible on iOS whose dismissal native has not yet reported. It
|
|
328
332
|
* stays committed after `visible` goes false until `topDismiss`, as React Native's Modal.js
|
|
@@ -830,6 +834,12 @@ export declare class Engine implements HostEngine {
|
|
|
830
834
|
* changes it too, by keeping or dropping the space before it.
|
|
831
835
|
*/
|
|
832
836
|
private markTextContent;
|
|
837
|
+
/**
|
|
838
|
+
* Text came, went or changed in a paragraph: where it has become one word, or stopped being
|
|
839
|
+
* one, the paragraph is merged again, since that is one line or as many as it takes. Asked
|
|
840
|
+
* of the paragraph's outermost element, which is the view; any other change leaves it be.
|
|
841
|
+
*/
|
|
842
|
+
private markWords;
|
|
833
843
|
/** A subtree that went out of the tree is back in: its hoisted nodes commit again. */
|
|
834
844
|
private wakeHoists;
|
|
835
845
|
/**
|
|
@@ -870,6 +880,11 @@ export declare class Engine implements HostEngine {
|
|
|
870
880
|
parentNode(node: EngineNode): EngineNode | null;
|
|
871
881
|
nextSibling(node: EngineNode): EngineNode | null;
|
|
872
882
|
setProp(node: EngineNode, key: string, value: unknown): void;
|
|
883
|
+
/**
|
|
884
|
+
* Have the paragraph a run of text is in merged again: it is as tall as the tallest line of
|
|
885
|
+
* its runs, by a line height or a text scaling set on one of them. See `lineTall`.
|
|
886
|
+
*/
|
|
887
|
+
private markParagraph;
|
|
873
888
|
/**
|
|
874
889
|
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
875
890
|
* `background-color` and `background-image` in the cascade, where a stylesheet wrote them, and
|
|
@@ -926,6 +941,23 @@ export declare class Engine implements HostEngine {
|
|
|
926
941
|
* which would restyle everything under them. Only sheets that use `:has()` pay.
|
|
927
942
|
*/
|
|
928
943
|
private markBeneath;
|
|
944
|
+
/**
|
|
945
|
+
* The boxes rules ask `:has()` of above the node they style, `.card:has(.x) .title`, by the
|
|
946
|
+
* sheet that asks: a sheet replaced or taken away asks no more.
|
|
947
|
+
*/
|
|
948
|
+
private readonly hasReaders;
|
|
949
|
+
/** The nodes something changed beneath since the last commit, where any sheet has such a rule. */
|
|
950
|
+
private readonly changedBeneath;
|
|
951
|
+
/**
|
|
952
|
+
* Have matched again what is under each box that changed beneath and that such a rule could
|
|
953
|
+
* be asking: the nodes the rule is for, and no other. Once for a commit, however many changes
|
|
954
|
+
* there were beneath the box: a table gains its rows one at a time.
|
|
955
|
+
*/
|
|
956
|
+
private markUnderHas;
|
|
957
|
+
/**
|
|
958
|
+
* What `node` matches changed, which the elements after it can read: as many of them as a rule
|
|
959
|
+
* steps along from one, every one for a `~`, and none where no sheet steps at all.
|
|
960
|
+
*/
|
|
929
961
|
private markLaterSiblings;
|
|
930
962
|
/** `node`'s child list changed: `moved` came into it at `at`, or went out of it from there. */
|
|
931
963
|
private markStructure;
|
|
@@ -1056,6 +1088,40 @@ export declare class Engine implements HostEngine {
|
|
|
1056
1088
|
/** What a touch on an element that keeps a drag holds off, while it lasts: see `holdSwipe`. */
|
|
1057
1089
|
private heldFor;
|
|
1058
1090
|
private mergeProps;
|
|
1091
|
+
/**
|
|
1092
|
+
* The z-index a view is drawn at for the boxes in it: the highest of theirs. A browser draws a
|
|
1093
|
+
* box with a z-index over what follows the boxes it is in, and a native view is ordered among
|
|
1094
|
+
* the views beside it and no further, so each view the box is in is raised as far. Read from
|
|
1095
|
+
* what its children committed, which for a raised child is what raised it. A child with a
|
|
1096
|
+
* z-index of its own answers with that and nothing from inside it, as the stacking context it
|
|
1097
|
+
* is. Only a plain view is raised: a scroll view clips what is in it, and a screen is ordered
|
|
1098
|
+
* by its stack.
|
|
1099
|
+
*
|
|
1100
|
+
* ponytail: a z-index is the one thing read as making a stacking context. Opacity and a
|
|
1101
|
+
* transform make one in a browser too; read them here if a box is ever raised past one.
|
|
1102
|
+
*/
|
|
1103
|
+
private raisedBy;
|
|
1104
|
+
/** Read how far a view is raised, once its children are committed: its props are, if it moved. */
|
|
1105
|
+
private raise;
|
|
1106
|
+
/**
|
|
1107
|
+
* Draw a border that has a width and no colour in the colour of the text, as a browser does:
|
|
1108
|
+
* `border-color` starts as `currentColor`, and native starts it as black. Each side with a
|
|
1109
|
+
* width, where nothing gave that side a colour. Left to native where there is no text colour
|
|
1110
|
+
* to take, which is black in a browser too.
|
|
1111
|
+
*/
|
|
1112
|
+
private borderInText;
|
|
1113
|
+
/**
|
|
1114
|
+
* The tallest line, as the system text size draws it, of the runs of text in a paragraph:
|
|
1115
|
+
* none is 0. A run scales as what it is in does, `within`, but for what it says of that
|
|
1116
|
+
* itself: one that does not scale, or stops at a size, beside text that does.
|
|
1117
|
+
*/
|
|
1118
|
+
private tallestRun;
|
|
1119
|
+
/**
|
|
1120
|
+
* Keep a paragraph whose text has nowhere to wrap on one line, as a browser does: a word is
|
|
1121
|
+
* not broken inside, and what does not fit is past the edge. Native breaks a word wider than
|
|
1122
|
+
* its line between any two letters. Cut at the edge, where no rule says how it ends.
|
|
1123
|
+
*/
|
|
1124
|
+
private wholeWords;
|
|
1059
1125
|
/**
|
|
1060
1126
|
* Leave a node's background out of what it commits when a child paints it, and give that child
|
|
1061
1127
|
* its parent's: the cascade's, and over it the parent's inline style.
|
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, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, placeReadElsewhere, siblingReach, } from "./css.js";
|
|
10
|
+
import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, hasAbove, 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 { aimsElsewhere, animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
|
|
@@ -867,6 +867,29 @@ function ratioForAutoSize(props) {
|
|
|
867
867
|
delete props['aspectRatio'];
|
|
868
868
|
}
|
|
869
869
|
const OUTLINE_KEYS = ['outlineWidth', 'outlineStyle', 'outlineColor', 'outlineOffset'];
|
|
870
|
+
/** Whether a side of a border has a width and nothing that gives it a colour. */
|
|
871
|
+
function colourless(props, side, flipped) {
|
|
872
|
+
const width = props[side.width] ?? props['borderWidth'];
|
|
873
|
+
if (typeof width !== 'number' || width <= 0)
|
|
874
|
+
return false;
|
|
875
|
+
const given = [side.color, flipped ? side.flipped : side.logical, 'borderColor'];
|
|
876
|
+
return given.every((key) => props[key] == null);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Each side a border has: its width, its colour, and the logical colour React Native takes for
|
|
880
|
+
* it too, read left to right and, `flipped`, right to left.
|
|
881
|
+
*/
|
|
882
|
+
const BORDER_SIDES = [
|
|
883
|
+
['Top', 'BlockStart', 'BlockStart'],
|
|
884
|
+
['Right', 'End', 'Start'],
|
|
885
|
+
['Bottom', 'BlockEnd', 'BlockEnd'],
|
|
886
|
+
['Left', 'Start', 'End'],
|
|
887
|
+
].map(([side, logical, flipped]) => ({
|
|
888
|
+
width: `border${side}Width`,
|
|
889
|
+
color: `border${side}Color`,
|
|
890
|
+
logical: `border${logical}Color`,
|
|
891
|
+
flipped: `border${flipped}Color`,
|
|
892
|
+
}));
|
|
870
893
|
/**
|
|
871
894
|
* Send nothing for an outline of no width, which is what `outline: none` is: it draws nothing,
|
|
872
895
|
* and a view with no outline is the same view. Sent, it is an update that is not one, and on
|
|
@@ -918,6 +941,64 @@ function alignMultiline(viewName, props) {
|
|
|
918
941
|
return;
|
|
919
942
|
props['textAlignVertical'] ??= 'top';
|
|
920
943
|
}
|
|
944
|
+
/** The props of a run of text that change how tall its line is drawn. */
|
|
945
|
+
const SIZES_A_RUN = new Set(['style', 'allowFontScaling', 'maxFontSizeMultiplier']);
|
|
946
|
+
/**
|
|
947
|
+
* Keep a paragraph as tall as one of its lines, as a browser's line box is whatever holds it.
|
|
948
|
+
* Yoga measures an item no taller than the room inside its container, and the text view cuts
|
|
949
|
+
* its letters off there: a 24 point line in a row with 16 points between its paddings. The
|
|
950
|
+
* line is the paragraph's own or a taller one of a run in it, `run`, which is scaled already. With it goes what is
|
|
951
|
+
* around the text and inside the height Yoga gives a view, its padding and border above and
|
|
952
|
+
* below, but for a content box. Only where a line height is a length, and nothing gives the
|
|
953
|
+
* paragraph a height of its own.
|
|
954
|
+
*
|
|
955
|
+
* The line is as the system text size draws it: native scales a line height with the text, so
|
|
956
|
+
* a line held to its points at a smaller size is shorter than its box and sits at the top of
|
|
957
|
+
* it, over what a row centres it beside.
|
|
958
|
+
*/
|
|
959
|
+
function lineTall(props, run, fontScale) {
|
|
960
|
+
const own = props['lineHeight'];
|
|
961
|
+
const line = Math.max(typeof own === 'number' ? own * textScale(props, fontScale) : 0, run);
|
|
962
|
+
if (line <= 0)
|
|
963
|
+
return;
|
|
964
|
+
const unset = (value) => value == null || value === 'auto';
|
|
965
|
+
if (!unset(props['height']) || !unset(props['minHeight']))
|
|
966
|
+
return;
|
|
967
|
+
const around = props['boxSizing'] === 'content-box' ? 0 : aroundALine(props);
|
|
968
|
+
// To a thousandth of a point: a product of two fractions is rarely the number it reads as.
|
|
969
|
+
props['minHeight'] = Math.round((line + around) * 1000) / 1000;
|
|
970
|
+
}
|
|
971
|
+
/** The points of padding and border above and below a view's content, where each is a length. */
|
|
972
|
+
function aroundALine(props) {
|
|
973
|
+
const edge = (...keys) => {
|
|
974
|
+
const found = keys.map((key) => props[key]).find((value) => value != null);
|
|
975
|
+
return typeof found === 'number' ? found : 0;
|
|
976
|
+
};
|
|
977
|
+
return (edge('paddingTop', 'paddingVertical', 'padding') +
|
|
978
|
+
edge('paddingBottom', 'paddingVertical', 'padding') +
|
|
979
|
+
edge('borderTopWidth', 'borderWidth') +
|
|
980
|
+
edge('borderBottomWidth', 'borderWidth'));
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Where a line may break: at a space, after a hyphen, and anywhere in a script written without
|
|
984
|
+
* spaces. A soft hyphen and a zero-width space are places to break that draw nothing.
|
|
985
|
+
*/
|
|
986
|
+
const BREAKS = /[\s\-\u00ad\u200b\u2010\u2e80-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/;
|
|
987
|
+
/** Whether a paragraph's text, all its runs together, is one word: something, with no break. */
|
|
988
|
+
function unbroken(node) {
|
|
989
|
+
let text = '';
|
|
990
|
+
const collect = (from) => {
|
|
991
|
+
for (const child of from.children) {
|
|
992
|
+
if (child.kind === 'text')
|
|
993
|
+
text += child.text;
|
|
994
|
+
else if (isTextElement(child))
|
|
995
|
+
collect(child);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
collect(node);
|
|
999
|
+
const words = trimEnds(text);
|
|
1000
|
+
return words !== '' && !BREAKS.test(words);
|
|
1001
|
+
}
|
|
921
1002
|
/**
|
|
922
1003
|
* Resolve a paragraph's `text-align` against its direction, as CSS does.
|
|
923
1004
|
*
|
|
@@ -1574,6 +1655,7 @@ function noteTouches(node, style) {
|
|
|
1574
1655
|
if (value === 'none')
|
|
1575
1656
|
node.propsDirty = true;
|
|
1576
1657
|
}
|
|
1658
|
+
const hasClasses = (node, names) => names.every((name) => node.classes?.has(name));
|
|
1577
1659
|
/** Whether `node` is `ancestor` or somewhere under it. */
|
|
1578
1660
|
function isWithin(node, ancestor) {
|
|
1579
1661
|
for (let at = node; at; at = at.parent)
|
|
@@ -1677,6 +1759,8 @@ class RetainedNode {
|
|
|
1677
1759
|
committedUnder = null;
|
|
1678
1760
|
kept = undefined;
|
|
1679
1761
|
remeasure = undefined;
|
|
1762
|
+
oneWord = undefined;
|
|
1763
|
+
raised = undefined;
|
|
1680
1764
|
propsDirty = false;
|
|
1681
1765
|
structureDirty = false;
|
|
1682
1766
|
subtreeDirty = false;
|
|
@@ -2352,12 +2436,16 @@ export class Engine {
|
|
|
2352
2436
|
*/
|
|
2353
2437
|
fontsSettled(families) {
|
|
2354
2438
|
let held = false;
|
|
2439
|
+
const release = (node) => {
|
|
2440
|
+
if (node?.heldFamily === undefined || !families.has(node.heldFamily))
|
|
2441
|
+
return;
|
|
2442
|
+
this.markProps(node, false);
|
|
2443
|
+
held = true;
|
|
2444
|
+
};
|
|
2355
2445
|
const visit = (node) => {
|
|
2356
2446
|
for (const child of node.children) {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
held = true;
|
|
2360
|
-
}
|
|
2447
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
2448
|
+
release(child.kind === 'text' ? child.box : child);
|
|
2361
2449
|
visit(child);
|
|
2362
2450
|
}
|
|
2363
2451
|
};
|
|
@@ -2366,22 +2454,28 @@ export class Engine {
|
|
|
2366
2454
|
this.commit();
|
|
2367
2455
|
}
|
|
2368
2456
|
fontsRegistered(families) {
|
|
2457
|
+
/** Lay `node` out again where it is text naming one of the families, saying whether it was. */
|
|
2458
|
+
const refresh = (node) => {
|
|
2459
|
+
// As it is drawn: a text element placing its text is a view, with the paragraph inside it.
|
|
2460
|
+
const viewName = committedViewName(node);
|
|
2461
|
+
const input = TEXT_INPUTS.has(viewName);
|
|
2462
|
+
const named = (viewName === PARAGRAPH || input) && namesFamily(node, families);
|
|
2463
|
+
if (!node.committed || !named)
|
|
2464
|
+
return false;
|
|
2465
|
+
this.fontRefreshes.set(node, (this.fontRefreshes.get(node) ?? 0) + 1);
|
|
2466
|
+
if (input)
|
|
2467
|
+
this.fontResends.add(node);
|
|
2468
|
+
this.fontsRefreshed = true;
|
|
2469
|
+
this.markProps(node, false);
|
|
2470
|
+
return true;
|
|
2471
|
+
};
|
|
2369
2472
|
const visit = (node) => {
|
|
2370
2473
|
for (const child of node.children) {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
if (child.committed && (viewName === PARAGRAPH || input) && namesFamily(child, families)) {
|
|
2376
|
-
this.fontRefreshes.set(child, (this.fontRefreshes.get(child) ?? 0) + 1);
|
|
2377
|
-
if (input)
|
|
2378
|
-
this.fontResends.add(child);
|
|
2379
|
-
this.fontsRefreshed = true;
|
|
2380
|
-
this.markProps(child, false);
|
|
2381
|
-
}
|
|
2382
|
-
else {
|
|
2474
|
+
// The paragraph of a run of text written straight into a view is kept on the text.
|
|
2475
|
+
if (child.kind === 'text' && child.box)
|
|
2476
|
+
refresh(child.box);
|
|
2477
|
+
if (child.kind === 'element' && !refresh(child))
|
|
2383
2478
|
visit(child);
|
|
2384
|
-
}
|
|
2385
2479
|
}
|
|
2386
2480
|
};
|
|
2387
2481
|
visit(this.root);
|
|
@@ -2588,7 +2682,8 @@ export class Engine {
|
|
|
2588
2682
|
*/
|
|
2589
2683
|
setDefaultStyle(node, style) {
|
|
2590
2684
|
node.defaultStyle = style;
|
|
2591
|
-
|
|
2685
|
+
// Under what the cascade comes to, and read by no selector: nothing is matched again.
|
|
2686
|
+
this.markProps(node, false);
|
|
2592
2687
|
}
|
|
2593
2688
|
/**
|
|
2594
2689
|
* `class="a b"` from a template. Replaces the set rather than adding to it. The same classes in
|
|
@@ -2667,8 +2762,27 @@ export class Engine {
|
|
|
2667
2762
|
* changes it too, by keeping or dropping the space before it.
|
|
2668
2763
|
*/
|
|
2669
2764
|
markTextContent(parent, child) {
|
|
2670
|
-
if (child.kind
|
|
2765
|
+
if (child.kind === 'anchor')
|
|
2766
|
+
return;
|
|
2767
|
+
if (takesTextAsProp(parent))
|
|
2671
2768
|
this.markProps(parent);
|
|
2769
|
+
else if (isTextElement(parent))
|
|
2770
|
+
this.markWords(parent, child.kind === 'element');
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Text came, went or changed in a paragraph: where it has become one word, or stopped being
|
|
2774
|
+
* one, the paragraph is merged again, since that is one line or as many as it takes. Asked
|
|
2775
|
+
* of the paragraph's outermost element, which is the view; any other change leaves it be.
|
|
2776
|
+
*/
|
|
2777
|
+
markWords(within, run = false) {
|
|
2778
|
+
let root = within;
|
|
2779
|
+
while (isTextElement(root.parent))
|
|
2780
|
+
root = root.parent;
|
|
2781
|
+
// A run that came or went can be the tallest line of the paragraph: see `lineTall`.
|
|
2782
|
+
// ponytail: one restyled to another line height is not; mark its paragraph if seen cut.
|
|
2783
|
+
const words = root.oneWord !== undefined && unbroken(root) !== root.oneWord;
|
|
2784
|
+
if (run || words)
|
|
2785
|
+
this.markProps(root, false);
|
|
2672
2786
|
}
|
|
2673
2787
|
/** A subtree that went out of the tree is back in: its hoisted nodes commit again. */
|
|
2674
2788
|
wakeHoists(node) {
|
|
@@ -2762,6 +2876,11 @@ export class Engine {
|
|
|
2762
2876
|
this.stopNative(node, node.playing);
|
|
2763
2877
|
node.playing = undefined;
|
|
2764
2878
|
this.stopScrolled(node);
|
|
2879
|
+
// And what follows it, where it is the scroll view: each starts again if it is put back.
|
|
2880
|
+
for (const follower of this.scrollTimelines.get(node) ?? []) {
|
|
2881
|
+
this.stopScrolled(follower);
|
|
2882
|
+
this.markProps(follower, false);
|
|
2883
|
+
}
|
|
2765
2884
|
this.running.delete(node);
|
|
2766
2885
|
this.playing.delete(node);
|
|
2767
2886
|
this.hoisted.delete(node);
|
|
@@ -2806,6 +2925,19 @@ export class Engine {
|
|
|
2806
2925
|
// field's text on every key, match nothing they did not.
|
|
2807
2926
|
const read = key === 'style' ? this.inlineReachesStyle(node) : this.styles.reads(key);
|
|
2808
2927
|
this.markProps(node, read || heirs);
|
|
2928
|
+
if (SIZES_A_RUN.has(key))
|
|
2929
|
+
this.markParagraph(node);
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Have the paragraph a run of text is in merged again: it is as tall as the tallest line of
|
|
2933
|
+
* its runs, by a line height or a text scaling set on one of them. See `lineTall`.
|
|
2934
|
+
*/
|
|
2935
|
+
markParagraph(run) {
|
|
2936
|
+
let paragraph = run;
|
|
2937
|
+
while (paragraph.parent && isTextElement(paragraph.parent))
|
|
2938
|
+
paragraph = paragraph.parent;
|
|
2939
|
+
if (paragraph !== run)
|
|
2940
|
+
this.markProps(paragraph, false);
|
|
2809
2941
|
}
|
|
2810
2942
|
/**
|
|
2811
2943
|
* Paint a node's background on one of its children instead of on the node: the node keeps its
|
|
@@ -2861,6 +2993,7 @@ export class Engine {
|
|
|
2861
2993
|
*/
|
|
2862
2994
|
styleChanged(node) {
|
|
2863
2995
|
this.markProps(node, this.inlineReachesStyle(node));
|
|
2996
|
+
this.markParagraph(node);
|
|
2864
2997
|
}
|
|
2865
2998
|
/**
|
|
2866
2999
|
* A bound declaration, `[style.color]`, whose value may be a `var()`. One that is, is kept as a
|
|
@@ -2956,7 +3089,11 @@ export class Engine {
|
|
|
2956
3089
|
}
|
|
2957
3090
|
/** Start matching ancestors again on a change, once a sheet that uses `:has()` is in play. */
|
|
2958
3091
|
watchHas(sheet) {
|
|
2959
|
-
if (!sheet?.has
|
|
3092
|
+
if (!sheet?.has)
|
|
3093
|
+
return;
|
|
3094
|
+
if (!this.hasReaders.has(sheet))
|
|
3095
|
+
this.hasReaders.set(sheet, hasAbove(sheet));
|
|
3096
|
+
if (this.hasSheets)
|
|
2960
3097
|
return;
|
|
2961
3098
|
this.hasSheets = true;
|
|
2962
3099
|
this.styles.tracksHas = true;
|
|
@@ -2973,15 +3110,53 @@ export class Engine {
|
|
|
2973
3110
|
* which would restyle everything under them. Only sheets that use `:has()` pay.
|
|
2974
3111
|
*/
|
|
2975
3112
|
markBeneath(from) {
|
|
2976
|
-
for (let node = from; node; node = node.parent)
|
|
3113
|
+
for (let node = from; node; node = node.parent) {
|
|
2977
3114
|
node.hasDirty = true;
|
|
3115
|
+
if (this.hasReaders.size)
|
|
3116
|
+
this.changedBeneath.add(node);
|
|
3117
|
+
}
|
|
2978
3118
|
}
|
|
3119
|
+
/**
|
|
3120
|
+
* The boxes rules ask `:has()` of above the node they style, `.card:has(.x) .title`, by the
|
|
3121
|
+
* sheet that asks: a sheet replaced or taken away asks no more.
|
|
3122
|
+
*/
|
|
3123
|
+
hasReaders = new Map();
|
|
3124
|
+
/** The nodes something changed beneath since the last commit, where any sheet has such a rule. */
|
|
3125
|
+
changedBeneath = new Set();
|
|
3126
|
+
/**
|
|
3127
|
+
* Have matched again what is under each box that changed beneath and that such a rule could
|
|
3128
|
+
* be asking: the nodes the rule is for, and no other. Once for a commit, however many changes
|
|
3129
|
+
* there were beneath the box: a table gains its rows one at a time.
|
|
3130
|
+
*/
|
|
3131
|
+
markUnderHas() {
|
|
3132
|
+
if (!this.changedBeneath.size)
|
|
3133
|
+
return;
|
|
3134
|
+
for (const node of this.changedBeneath) {
|
|
3135
|
+
for (const readers of this.hasReaders.values()) {
|
|
3136
|
+
for (const { classes, subjects } of readers) {
|
|
3137
|
+
if (classes.every((name) => node.classes?.has(name)))
|
|
3138
|
+
this.markUnder(node, subjects);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
this.changedBeneath.clear();
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* What `node` matches changed, which the elements after it can read: as many of them as a rule
|
|
3146
|
+
* steps along from one, every one for a `~`, and none where no sheet steps at all.
|
|
3147
|
+
*/
|
|
2979
3148
|
markLaterSiblings(node) {
|
|
2980
|
-
|
|
3149
|
+
let left = this.structuralAfter;
|
|
3150
|
+
const siblings = left > 0 ? node.parent?.children : undefined;
|
|
2981
3151
|
if (!siblings)
|
|
2982
3152
|
return;
|
|
2983
|
-
|
|
2984
|
-
|
|
3153
|
+
// Each matched again, and left as it was with all under it where it matches what it did:
|
|
3154
|
+
// see `markPlace`.
|
|
3155
|
+
for (let i = siblings.indexOf(node) + 1; i < siblings.length && left > 0; i++) {
|
|
3156
|
+
const sibling = siblings[i];
|
|
3157
|
+
this.markPlace(sibling);
|
|
3158
|
+
if (sibling.kind === 'element')
|
|
3159
|
+
left--;
|
|
2985
3160
|
}
|
|
2986
3161
|
}
|
|
2987
3162
|
/** `node`'s child list changed: `moved` came into it at `at`, or went out of it from there. */
|
|
@@ -3056,9 +3231,16 @@ export class Engine {
|
|
|
3056
3231
|
* which every such compound it could be asks for: a compound with none could be any element.
|
|
3057
3232
|
*/
|
|
3058
3233
|
placeReadFromUnder(node) {
|
|
3059
|
-
for (const compound of this.placeElsewhere) {
|
|
3060
|
-
if (
|
|
3234
|
+
for (const { compound, above, child } of this.placeElsewhere) {
|
|
3235
|
+
if (!hasClasses(node, compound.classes))
|
|
3236
|
+
continue;
|
|
3237
|
+
// And by what the rule has it in, which a compound with no class is told from.
|
|
3238
|
+
if (!above?.length)
|
|
3061
3239
|
return true;
|
|
3240
|
+
for (let up = node.parent; up; up = child ? null : up.parent) {
|
|
3241
|
+
if (hasClasses(up, above))
|
|
3242
|
+
return true;
|
|
3243
|
+
}
|
|
3062
3244
|
}
|
|
3063
3245
|
return false;
|
|
3064
3246
|
}
|
|
@@ -3109,6 +3291,7 @@ export class Engine {
|
|
|
3109
3291
|
// completeRoot that changes nothing: during a fling that is one wasted native commit per
|
|
3110
3292
|
// frame, since any listener firing marks the view for refresh.
|
|
3111
3293
|
this.settleCreated();
|
|
3294
|
+
this.markUnderHas();
|
|
3112
3295
|
if (!this.root.structureDirty && !this.root.subtreeDirty)
|
|
3113
3296
|
return false;
|
|
3114
3297
|
const started = now();
|
|
@@ -3358,8 +3541,11 @@ export class Engine {
|
|
|
3358
3541
|
flattenStyle(node.props[STYLE_OVERRIDE], style);
|
|
3359
3542
|
this.fontFaces.apply(style);
|
|
3360
3543
|
holdLoadingFamily(node, style);
|
|
3361
|
-
if (viewName === PARAGRAPH)
|
|
3544
|
+
if (viewName === PARAGRAPH) {
|
|
3362
3545
|
alignText(style, this.directionOf(node, style));
|
|
3546
|
+
this.wholeWords(node, style);
|
|
3547
|
+
lineTall(style, this.tallestRun(node, style, 0), this.fontScale);
|
|
3548
|
+
}
|
|
3363
3549
|
if (this.fontsRefreshed)
|
|
3364
3550
|
this.capForFonts(node, style);
|
|
3365
3551
|
alignMultiline(viewName, style);
|
|
@@ -3375,6 +3561,7 @@ export class Engine {
|
|
|
3375
3561
|
// After the basis, which is a size given where it is committed as one.
|
|
3376
3562
|
ratioForAutoSize(merged);
|
|
3377
3563
|
noOutline(merged);
|
|
3564
|
+
this.borderInText(node, merged);
|
|
3378
3565
|
// Last, on what is committed: an override or an animation can hide a box, or place it.
|
|
3379
3566
|
hiddenOutOfFlow(merged);
|
|
3380
3567
|
delete merged['touchAction'];
|
|
@@ -3382,8 +3569,103 @@ export class Engine {
|
|
|
3382
3569
|
stillTouched(merged);
|
|
3383
3570
|
this.heldFor(node, merged);
|
|
3384
3571
|
this.movePaint(node, merged);
|
|
3572
|
+
if (node.raised !== undefined)
|
|
3573
|
+
merged['zIndex'] ??= node.raised;
|
|
3385
3574
|
return merged;
|
|
3386
3575
|
}
|
|
3576
|
+
/**
|
|
3577
|
+
* The z-index a view is drawn at for the boxes in it: the highest of theirs. A browser draws a
|
|
3578
|
+
* box with a z-index over what follows the boxes it is in, and a native view is ordered among
|
|
3579
|
+
* the views beside it and no further, so each view the box is in is raised as far. Read from
|
|
3580
|
+
* what its children committed, which for a raised child is what raised it. A child with a
|
|
3581
|
+
* z-index of its own answers with that and nothing from inside it, as the stacking context it
|
|
3582
|
+
* is. Only a plain view is raised: a scroll view clips what is in it, and a screen is ordered
|
|
3583
|
+
* by its stack.
|
|
3584
|
+
*
|
|
3585
|
+
* ponytail: a z-index is the one thing read as making a stacking context. Opacity and a
|
|
3586
|
+
* transform make one in a browser too; read them here if a box is ever raised past one.
|
|
3587
|
+
*/
|
|
3588
|
+
raisedBy(node, viewName) {
|
|
3589
|
+
if (viewName !== DEFAULT_VIEW)
|
|
3590
|
+
return undefined;
|
|
3591
|
+
let over = 0;
|
|
3592
|
+
for (const child of node.children) {
|
|
3593
|
+
const z = child.committed?.props['zIndex'];
|
|
3594
|
+
if (typeof z !== 'number' || z <= over)
|
|
3595
|
+
continue;
|
|
3596
|
+
if (!this.committedElsewhere(child) && !this.withheld(child))
|
|
3597
|
+
over = z;
|
|
3598
|
+
}
|
|
3599
|
+
return over || undefined;
|
|
3600
|
+
}
|
|
3601
|
+
/** Read how far a view is raised, once its children are committed: its props are, if it moved. */
|
|
3602
|
+
raise(node, viewName) {
|
|
3603
|
+
const raised = this.raisedBy(node, viewName);
|
|
3604
|
+
if (raised === node.raised)
|
|
3605
|
+
return;
|
|
3606
|
+
node.raised = raised;
|
|
3607
|
+
node.propsDirty = true;
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Draw a border that has a width and no colour in the colour of the text, as a browser does:
|
|
3611
|
+
* `border-color` starts as `currentColor`, and native starts it as black. Each side with a
|
|
3612
|
+
* width, where nothing gave that side a colour. Left to native where there is no text colour
|
|
3613
|
+
* to take, which is black in a browser too.
|
|
3614
|
+
*/
|
|
3615
|
+
borderInText(node, props) {
|
|
3616
|
+
let text;
|
|
3617
|
+
// The side a logical colour is for is the other one where text is read right to left.
|
|
3618
|
+
const flipped = (props['borderStartColor'] != null || props['borderEndColor'] != null) &&
|
|
3619
|
+
this.styles.readsRightToLeft(props);
|
|
3620
|
+
for (const side of BORDER_SIDES) {
|
|
3621
|
+
if (!colourless(props, side, flipped))
|
|
3622
|
+
continue;
|
|
3623
|
+
text ??= props['color'] ?? this.styles.resolve(node, this.styleEpoch).inherited['color'];
|
|
3624
|
+
if (text == null)
|
|
3625
|
+
return;
|
|
3626
|
+
props[side.color] = text;
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* The tallest line, as the system text size draws it, of the runs of text in a paragraph:
|
|
3631
|
+
* none is 0. A run scales as what it is in does, `within`, but for what it says of that
|
|
3632
|
+
* itself: one that does not scale, or stops at a size, beside text that does.
|
|
3633
|
+
*/
|
|
3634
|
+
tallestRun(node, within, tallest) {
|
|
3635
|
+
for (const child of node.children) {
|
|
3636
|
+
if (!isTextElement(child))
|
|
3637
|
+
continue;
|
|
3638
|
+
const scaling = {
|
|
3639
|
+
allowFontScaling: child.props['allowFontScaling'] ?? within['allowFontScaling'],
|
|
3640
|
+
maxFontSizeMultiplier: child.props['maxFontSizeMultiplier'] ?? within['maxFontSizeMultiplier'],
|
|
3641
|
+
};
|
|
3642
|
+
const line = inlineOf(child)['lineHeight'] ??
|
|
3643
|
+
this.styles.resolve(child, this.styleEpoch).style['lineHeight'];
|
|
3644
|
+
const drawn = typeof line === 'number' ? line * textScale(scaling, this.fontScale) : 0;
|
|
3645
|
+
tallest = this.tallestRun(child, scaling, Math.max(tallest, drawn));
|
|
3646
|
+
}
|
|
3647
|
+
return tallest;
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Keep a paragraph whose text has nowhere to wrap on one line, as a browser does: a word is
|
|
3651
|
+
* not broken inside, and what does not fit is past the edge. Native breaks a word wider than
|
|
3652
|
+
* its line between any two letters. Cut at the edge, where no rule says how it ends.
|
|
3653
|
+
*/
|
|
3654
|
+
wholeWords(node, props) {
|
|
3655
|
+
// Remembered, for its text changing to be told from its text changing what this answers.
|
|
3656
|
+
node.oneWord = props['numberOfLines'] == null ? unbroken(node) : undefined;
|
|
3657
|
+
if (!node.oneWord)
|
|
3658
|
+
return;
|
|
3659
|
+
props['numberOfLines'] = 1;
|
|
3660
|
+
const ended = [
|
|
3661
|
+
this.styles.resolve(node, this.styleEpoch).style,
|
|
3662
|
+
node.props,
|
|
3663
|
+
inlineOf(node),
|
|
3664
|
+
flattenStyle(node.props[STYLE_OVERRIDE], {}),
|
|
3665
|
+
];
|
|
3666
|
+
if (ended.every((from) => from['ellipsizeMode'] == null))
|
|
3667
|
+
props['ellipsizeMode'] = 'clip';
|
|
3668
|
+
}
|
|
3387
3669
|
/**
|
|
3388
3670
|
* Leave a node's background out of what it commits when a child paints it, and give that child
|
|
3389
3671
|
* its parent's: the cascade's, and over it the parent's inline style.
|
|
@@ -3500,6 +3782,8 @@ export class Engine {
|
|
|
3500
3782
|
sheetReplaced(sheet, next) {
|
|
3501
3783
|
if (sheet === next && this.knownSheets.has(sheet))
|
|
3502
3784
|
return;
|
|
3785
|
+
this.hasReaders.delete(sheet);
|
|
3786
|
+
this.watchHas(next);
|
|
3503
3787
|
this.unwatchActive(sheet);
|
|
3504
3788
|
this.watchActive(next);
|
|
3505
3789
|
const at = this.sheetOrder.indexOf(sheet);
|
|
@@ -3927,7 +4211,7 @@ export class Engine {
|
|
|
3927
4211
|
/** Every node playing a scroll-driven animation, by the scroll view that plays it. */
|
|
3928
4212
|
scrollTimelines = new Map();
|
|
3929
4213
|
/** How far each such scroll view scrolls, along each axis, once one of its events has said. */
|
|
3930
|
-
scrollExtents = new
|
|
4214
|
+
scrollExtents = new WeakMap();
|
|
3931
4215
|
/**
|
|
3932
4216
|
* An animation played by the nearest scroll view's offset: `animation-timeline: scroll()`.
|
|
3933
4217
|
*
|
|
@@ -4013,8 +4297,13 @@ export class Engine {
|
|
|
4013
4297
|
return;
|
|
4014
4298
|
node.scrolled = undefined;
|
|
4015
4299
|
scrolled.drive?.stop();
|
|
4016
|
-
|
|
4017
|
-
|
|
4300
|
+
const playing = scrolled.source && this.scrollTimelines.get(scrolled.source);
|
|
4301
|
+
if (!playing)
|
|
4302
|
+
return;
|
|
4303
|
+
playing.delete(node);
|
|
4304
|
+
// The last to follow this scroll view: a key left here keeps it, and all it is linked to.
|
|
4305
|
+
if (!playing.size)
|
|
4306
|
+
this.scrollTimelines.delete(scrolled.source);
|
|
4018
4307
|
}
|
|
4019
4308
|
/**
|
|
4020
4309
|
* A scroll view has said how far it scrolls. Every animation on it with a range that depends on
|
|
@@ -4672,6 +4961,7 @@ export class Engine {
|
|
|
4672
4961
|
this.noteBaselineFields(node);
|
|
4673
4962
|
this.freshBaselines(node);
|
|
4674
4963
|
const childHandles = this.reconcileChildren(node, viewName, style);
|
|
4964
|
+
this.raise(node, viewName);
|
|
4675
4965
|
noteTouches(node, style);
|
|
4676
4966
|
const previous = node.committed;
|
|
4677
4967
|
this.notePresented(node, viewName);
|
|
@@ -4782,8 +5072,12 @@ export class Engine {
|
|
|
4782
5072
|
}
|
|
4783
5073
|
box.parent = parent;
|
|
4784
5074
|
// The text is not under the box in the tree, so a change to it never marked the box.
|
|
4785
|
-
if (!this.isClean(text))
|
|
5075
|
+
if (!this.isClean(text)) {
|
|
4786
5076
|
box.subtreeDirty = true;
|
|
5077
|
+
// Nor had it merged again where the text became one word, or stopped being one.
|
|
5078
|
+
if (box.oneWord !== undefined && unbroken(box) !== box.oneWord)
|
|
5079
|
+
box.propsDirty = true;
|
|
5080
|
+
}
|
|
4787
5081
|
return box;
|
|
4788
5082
|
}
|
|
4789
5083
|
/** Remember an iOS modal host committed as showing, so hiding it waits for `topDismiss`. */
|