@ng-native/fabric 0.1.1 → 0.1.3
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/README.md +1 -1
- package/dist/css.d.ts +64 -13
- package/dist/css.js +209 -46
- package/dist/css.js.map +1 -1
- package/dist/engine.d.ts +56 -3
- package/dist/engine.js +144 -9
- package/dist/engine.js.map +1 -1
- package/dist/font-faces.d.ts +42 -0
- package/dist/font-faces.js +112 -0
- package/dist/font-faces.js.map +1 -0
- package/dist/forms-inputs.js +1 -1
- package/dist/forms-inputs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/inline-token.d.ts +3 -2
- package/dist/inline-token.js +73 -1
- package/dist/inline-token.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Framework-agnostic retained tree + incremental Fabric commit.
|
|
3
|
-
* Nothing Angular-specific may enter this file; lint bans the import (see ARCHITECTURE.md).
|
|
3
|
+
* Nothing Angular-specific may enter this file; lint bans the import (see docs/ARCHITECTURE.md).
|
|
4
4
|
*
|
|
5
5
|
* Angular mutates; Fabric is persistent. A committed node is never mutated, it is cloned with
|
|
6
6
|
* new props and/or a new child set. Cloning a leaf therefore forces every ancestor to re-clone,
|
|
@@ -284,6 +284,14 @@ export interface PlatformViewName {
|
|
|
284
284
|
readonly ios: string;
|
|
285
285
|
readonly android: string;
|
|
286
286
|
}
|
|
287
|
+
/** Hear about faces registering with the platform, until the returned function is called. */
|
|
288
|
+
export declare function onFontsRegistered(listener: (families: ReadonlySet<string>) => void): () => void;
|
|
289
|
+
/**
|
|
290
|
+
* Faces just registered with the platform, by the family names text asks for them by. Every
|
|
291
|
+
* mounted app lays out again the paragraphs and text inputs that name one: see
|
|
292
|
+
* `Engine.fontsRegistered`.
|
|
293
|
+
*/
|
|
294
|
+
export declare function fontsRegistered(families: Iterable<string>): void;
|
|
287
295
|
/**
|
|
288
296
|
* Commit every `elementName` as a direct child of its nearest `ancestorName`, wherever the
|
|
289
297
|
* template put it, e.g. `registerHoist('native-header', 'screen')`.
|
|
@@ -503,6 +511,29 @@ export declare class Engine implements HostEngine {
|
|
|
503
511
|
* which is what makes Fabric dirty a measured node and measure it at the size now in effect.
|
|
504
512
|
*/
|
|
505
513
|
remeasureText(): void;
|
|
514
|
+
/** Whether any text has been laid out again for a face. See `fontsRegistered`. */
|
|
515
|
+
private fontsRefreshed;
|
|
516
|
+
/** How many times each paragraph or text input has been laid out again for a face. */
|
|
517
|
+
private readonly fontRefreshes;
|
|
518
|
+
/** The text inputs whose `fontFamily` the next commit sends again. See `fontsRegistered`. */
|
|
519
|
+
private readonly fontResends;
|
|
520
|
+
/**
|
|
521
|
+
* Faces registered after text naming them was laid out: lay that text out again, and commit.
|
|
522
|
+
*
|
|
523
|
+
* Native keeps a paragraph's text layout, face included, keyed by what the paragraph asks for.
|
|
524
|
+
* iOS caches the attributed string it built and the size it measured, so text laid out before
|
|
525
|
+
* its face registered stays in the fallback face, and committing it again unchanged hits the
|
|
526
|
+
* same cache. What it asks for has to change, and nothing visible can: each paragraph naming
|
|
527
|
+
* one of the families is committed with its text size cap raised by a step too small to see, or
|
|
528
|
+
* set far above any size the platform offers where it had none. Its view, and everything native
|
|
529
|
+
* holds for it, stays where it is. Android keys its text layout the same way and applies the
|
|
530
|
+
* cap the same way, so the same change works there.
|
|
531
|
+
*
|
|
532
|
+
* A text input takes its font another way. iOS sets it on the field again only when the input's
|
|
533
|
+
* text attributes change, which the same cap does. Android sets it only when `fontFamily` is in
|
|
534
|
+
* the props it is sent, and the engine sends only what changed, so it is sent again once.
|
|
535
|
+
*/
|
|
536
|
+
fontsRegistered(families: ReadonlySet<string>): void;
|
|
506
537
|
/**
|
|
507
538
|
* Put the cursor in a text input; bring anything else on screen, in the scroll view it is in.
|
|
508
539
|
* A form sends the user to the first field that needs fixing this way, and a toggle or a picker
|
|
@@ -661,6 +692,13 @@ export declare class Engine implements HostEngine {
|
|
|
661
692
|
* unrelated change.
|
|
662
693
|
*/
|
|
663
694
|
private mergeProps;
|
|
695
|
+
/**
|
|
696
|
+
* The text size cap that lays a paragraph or text input out again once a face it names has
|
|
697
|
+
* registered: a step per face above the app's own cap where it set one (below 1 is none, on
|
|
698
|
+
* both platforms), or above any text size there is. Counted per view, so an app's cap moves by
|
|
699
|
+
* a step for each face its text names and no further. See `fontsRegistered`.
|
|
700
|
+
*/
|
|
701
|
+
private capForFonts;
|
|
664
702
|
/** Set once any node has had a `direction` in its inline style. See `directionOf`. */
|
|
665
703
|
private inlineDirection;
|
|
666
704
|
/**
|
|
@@ -681,8 +719,21 @@ export declare class Engine implements HostEngine {
|
|
|
681
719
|
* A property seen for the first time is recorded and left alone. That is CSS's own rule, and
|
|
682
720
|
* without it every element would fade in from whatever the previous value happened to be.
|
|
683
721
|
*/
|
|
684
|
-
/**
|
|
685
|
-
|
|
722
|
+
/**
|
|
723
|
+
* Take a sheet's `@keyframes` and `@font-face` rules into the registries. Cheap and idempotent;
|
|
724
|
+
* sheets are few.
|
|
725
|
+
*/
|
|
726
|
+
private registerSheet;
|
|
727
|
+
/** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
|
|
728
|
+
private readonly fontFaces;
|
|
729
|
+
/** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
|
|
730
|
+
private facesAdded;
|
|
731
|
+
/**
|
|
732
|
+
* Faces a sheet declared partway through a commit: text resolved before it, in this commit or
|
|
733
|
+
* an earlier one, matched without them. Resolve every text naming a family again, and commit.
|
|
734
|
+
* Once per sheet that declares faces, so walking the tree costs nothing that matters.
|
|
735
|
+
*/
|
|
736
|
+
private rematchFonts;
|
|
686
737
|
private readonly knownSheets;
|
|
687
738
|
/**
|
|
688
739
|
* Start, advance or stop the `@keyframes` animation a node's style asks for.
|
|
@@ -769,6 +820,8 @@ export declare class Engine implements HostEngine {
|
|
|
769
820
|
private processGradients;
|
|
770
821
|
private clearFlags;
|
|
771
822
|
private reconcile;
|
|
823
|
+
/** What a clone sends: the props that changed, and a text input's font once it registered. */
|
|
824
|
+
private propsPayload;
|
|
772
825
|
/**
|
|
773
826
|
* Whether a node has to be committed with its children again.
|
|
774
827
|
*
|
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Framework-agnostic retained tree + incremental Fabric commit.
|
|
3
|
-
* Nothing Angular-specific may enter this file; lint bans the import (see ARCHITECTURE.md).
|
|
3
|
+
* Nothing Angular-specific may enter this file; lint bans the import (see docs/ARCHITECTURE.md).
|
|
4
4
|
*
|
|
5
5
|
* Angular mutates; Fabric is persistent. A committed node is never mutated, it is cloned with
|
|
6
6
|
* new props and/or a new child set. Cloning a leaf therefore forces every ancestor to re-clone,
|
|
@@ -14,6 +14,7 @@ import { transformList } from "./inline-transform.js";
|
|
|
14
14
|
import { tokenFromValue } from "./inline-token.js";
|
|
15
15
|
import { NativeScrollDriver, scrollFeed, } from "./native-drive.js";
|
|
16
16
|
import { firstFrame, rangeOf, scrollChannels } from "./scroll-animation.js";
|
|
17
|
+
import { FontFaces } from "./font-faces.js";
|
|
17
18
|
/**
|
|
18
19
|
* The animation a node's style asks for, with a play state from a rule of its own applied. Both
|
|
19
20
|
* are instructions for this engine and nothing native has heard of them, so they come out of the
|
|
@@ -380,6 +381,40 @@ function registeredViewName(elementName) {
|
|
|
380
381
|
* a paragraph, and a text input on either platform. See `Engine.remeasureText`.
|
|
381
382
|
*/
|
|
382
383
|
const MEASURED_VIEWS = new Set(['Paragraph', 'TextInput', 'AndroidTextInput']);
|
|
384
|
+
/** What each mounted app does when faces register. See `fontsRegistered`. */
|
|
385
|
+
const fontListeners = new Set();
|
|
386
|
+
/** Hear about faces registering with the platform, until the returned function is called. */
|
|
387
|
+
export function onFontsRegistered(listener) {
|
|
388
|
+
fontListeners.add(listener);
|
|
389
|
+
return () => fontListeners.delete(listener);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Faces just registered with the platform, by the family names text asks for them by. Every
|
|
393
|
+
* mounted app lays out again the paragraphs and text inputs that name one: see
|
|
394
|
+
* `Engine.fontsRegistered`.
|
|
395
|
+
*/
|
|
396
|
+
export function fontsRegistered(families) {
|
|
397
|
+
const names = new Set(families);
|
|
398
|
+
if (!names.size)
|
|
399
|
+
return;
|
|
400
|
+
for (const listener of [...fontListeners])
|
|
401
|
+
listener(names);
|
|
402
|
+
}
|
|
403
|
+
/** Whether a paragraph, or a span in it, was committed asking for one of these families. */
|
|
404
|
+
function namesFamily(node, families) {
|
|
405
|
+
const family = node.committed?.props['fontFamily'];
|
|
406
|
+
if (typeof family === 'string' && families.has(family))
|
|
407
|
+
return true;
|
|
408
|
+
return node.children.some((child) => namesFamily(child, families));
|
|
409
|
+
}
|
|
410
|
+
/** A text size cap far above any a platform offers, so it caps nothing. See `fontsRegistered`. */
|
|
411
|
+
const UNCAPPED = 1000;
|
|
412
|
+
/**
|
|
413
|
+
* How far a paragraph's cap moves each time a face it names registers: just past the 0.005 React
|
|
414
|
+
* Native compares text attributes to, so the layout it cached is not found, and too little for
|
|
415
|
+
* an app's own cap to cap anything visibly differently.
|
|
416
|
+
*/
|
|
417
|
+
const FONT_REFRESH_STEP = 0.006;
|
|
383
418
|
/** The views that take a cursor, on either platform. */
|
|
384
419
|
const TEXT_INPUTS = new Set(['TextInput', 'AndroidTextInput']);
|
|
385
420
|
/** The views that scroll their content, which `focus` brings a field into view within. */
|
|
@@ -947,7 +982,7 @@ export class Engine {
|
|
|
947
982
|
this.now = options.now ?? (() => globalThis.performance?.now?.() ?? Date.now());
|
|
948
983
|
this.onDirty = options.onDirty;
|
|
949
984
|
this.onError = options.onError;
|
|
950
|
-
this.
|
|
985
|
+
this.registerSheet(options.globalStyles);
|
|
951
986
|
this.processColor = options.processColor ?? ((value) => value);
|
|
952
987
|
this.resolveAssetSource = options.resolveAssetSource ?? ((value) => value);
|
|
953
988
|
this.scrollDriver = options.nativeAnimated
|
|
@@ -1045,6 +1080,50 @@ export class Engine {
|
|
|
1045
1080
|
visit(this.root);
|
|
1046
1081
|
this.commit();
|
|
1047
1082
|
}
|
|
1083
|
+
/** Whether any text has been laid out again for a face. See `fontsRegistered`. */
|
|
1084
|
+
fontsRefreshed = false;
|
|
1085
|
+
/** How many times each paragraph or text input has been laid out again for a face. */
|
|
1086
|
+
fontRefreshes = new WeakMap();
|
|
1087
|
+
/** The text inputs whose `fontFamily` the next commit sends again. See `fontsRegistered`. */
|
|
1088
|
+
fontResends = new WeakSet();
|
|
1089
|
+
/**
|
|
1090
|
+
* Faces registered after text naming them was laid out: lay that text out again, and commit.
|
|
1091
|
+
*
|
|
1092
|
+
* Native keeps a paragraph's text layout, face included, keyed by what the paragraph asks for.
|
|
1093
|
+
* iOS caches the attributed string it built and the size it measured, so text laid out before
|
|
1094
|
+
* its face registered stays in the fallback face, and committing it again unchanged hits the
|
|
1095
|
+
* same cache. What it asks for has to change, and nothing visible can: each paragraph naming
|
|
1096
|
+
* one of the families is committed with its text size cap raised by a step too small to see, or
|
|
1097
|
+
* set far above any size the platform offers where it had none. Its view, and everything native
|
|
1098
|
+
* holds for it, stays where it is. Android keys its text layout the same way and applies the
|
|
1099
|
+
* cap the same way, so the same change works there.
|
|
1100
|
+
*
|
|
1101
|
+
* A text input takes its font another way. iOS sets it on the field again only when the input's
|
|
1102
|
+
* text attributes change, which the same cap does. Android sets it only when `fontFamily` is in
|
|
1103
|
+
* the props it is sent, and the engine sends only what changed, so it is sent again once.
|
|
1104
|
+
*/
|
|
1105
|
+
fontsRegistered(families) {
|
|
1106
|
+
const visit = (node) => {
|
|
1107
|
+
for (const child of node.children) {
|
|
1108
|
+
if (child.kind !== 'element')
|
|
1109
|
+
continue;
|
|
1110
|
+
const viewName = viewNameOf(child);
|
|
1111
|
+
const input = TEXT_INPUTS.has(viewName);
|
|
1112
|
+
if (child.committed && (viewName === PARAGRAPH || input) && namesFamily(child, families)) {
|
|
1113
|
+
this.fontRefreshes.set(child, (this.fontRefreshes.get(child) ?? 0) + 1);
|
|
1114
|
+
if (input)
|
|
1115
|
+
this.fontResends.add(child);
|
|
1116
|
+
this.fontsRefreshed = true;
|
|
1117
|
+
this.markProps(child, false);
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
visit(child);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
visit(this.root);
|
|
1125
|
+
this.commit();
|
|
1126
|
+
}
|
|
1048
1127
|
/**
|
|
1049
1128
|
* Put the cursor in a text input; bring anything else on screen, in the scroll view it is in.
|
|
1050
1129
|
* A form sends the user to the first field that needs fixing this way, and a toggle or a picker
|
|
@@ -1428,6 +1507,8 @@ export class Engine {
|
|
|
1428
1507
|
// One epoch per commit. Node props do not change while a commit runs, so every node can be
|
|
1429
1508
|
// resolved once and shared with each of its descendants for the length of this walk.
|
|
1430
1509
|
this.styleEpoch++;
|
|
1510
|
+
// Faces registered before the walk, the global sheet's among them, reach every node in it.
|
|
1511
|
+
this.facesAdded = false;
|
|
1431
1512
|
const set = this.fabric.createChildSet(this.rootTag);
|
|
1432
1513
|
for (const child of this.visibleChildren(this.root)) {
|
|
1433
1514
|
this.fabric.appendChildToSet(set, this.reconcileUnder(this.root, child));
|
|
@@ -1448,6 +1529,8 @@ export class Engine {
|
|
|
1448
1529
|
}
|
|
1449
1530
|
this.flushTransitionEvents();
|
|
1450
1531
|
this.scrollDriver?.afterCommit();
|
|
1532
|
+
if (this.facesAdded)
|
|
1533
|
+
this.rematchFonts();
|
|
1451
1534
|
return true;
|
|
1452
1535
|
}
|
|
1453
1536
|
scrollDriver;
|
|
@@ -1533,8 +1616,8 @@ export class Engine {
|
|
|
1533
1616
|
return { text: paragraphText(node) };
|
|
1534
1617
|
if (this.dev)
|
|
1535
1618
|
this.checkProps(node);
|
|
1536
|
-
this.
|
|
1537
|
-
this.
|
|
1619
|
+
this.registerSheet(node.sheet);
|
|
1620
|
+
this.registerSheet(node.hostSheet);
|
|
1538
1621
|
const props = { ...DEFAULT_PROPS[viewName] };
|
|
1539
1622
|
Object.assign(props, this.styles.resolve(node, this.styleEpoch).style);
|
|
1540
1623
|
for (const key of Object.keys(node.props)) {
|
|
@@ -1552,10 +1635,27 @@ export class Engine {
|
|
|
1552
1635
|
if (intrinsic)
|
|
1553
1636
|
applyIntrinsicSize(style, intrinsic);
|
|
1554
1637
|
flattenStyle(node.props[STYLE_OVERRIDE], style);
|
|
1638
|
+
this.fontFaces.apply(style);
|
|
1555
1639
|
if (viewName === PARAGRAPH)
|
|
1556
1640
|
alignText(style, this.directionOf(node, style));
|
|
1641
|
+
if (this.fontsRefreshed)
|
|
1642
|
+
this.capForFonts(node, style);
|
|
1557
1643
|
return composeTransform(node, this.animated(node, this.transitioned(node, style)));
|
|
1558
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The text size cap that lays a paragraph or text input out again once a face it names has
|
|
1647
|
+
* registered: a step per face above the app's own cap where it set one (below 1 is none, on
|
|
1648
|
+
* both platforms), or above any text size there is. Counted per view, so an app's cap moves by
|
|
1649
|
+
* a step for each face its text names and no further. See `fontsRegistered`.
|
|
1650
|
+
*/
|
|
1651
|
+
capForFonts(node, props) {
|
|
1652
|
+
const refreshes = this.fontRefreshes.get(node);
|
|
1653
|
+
if (refreshes === undefined)
|
|
1654
|
+
return;
|
|
1655
|
+
const own = props['maxFontSizeMultiplier'];
|
|
1656
|
+
props['maxFontSizeMultiplier'] =
|
|
1657
|
+
(typeof own === 'number' && own >= 1 ? own : UNCAPPED) + refreshes * FONT_REFRESH_STEP;
|
|
1658
|
+
}
|
|
1559
1659
|
/** Set once any node has had a `direction` in its inline style. See `directionOf`. */
|
|
1560
1660
|
inlineDirection = false;
|
|
1561
1661
|
/**
|
|
@@ -1590,14 +1690,41 @@ export class Engine {
|
|
|
1590
1690
|
* A property seen for the first time is recorded and left alone. That is CSS's own rule, and
|
|
1591
1691
|
* without it every element would fade in from whatever the previous value happened to be.
|
|
1592
1692
|
*/
|
|
1593
|
-
/**
|
|
1594
|
-
|
|
1595
|
-
|
|
1693
|
+
/**
|
|
1694
|
+
* Take a sheet's `@keyframes` and `@font-face` rules into the registries. Cheap and idempotent;
|
|
1695
|
+
* sheets are few.
|
|
1696
|
+
*/
|
|
1697
|
+
registerSheet(sheet) {
|
|
1698
|
+
if (!sheet || this.knownSheets.has(sheet))
|
|
1596
1699
|
return;
|
|
1597
1700
|
this.knownSheets.add(sheet);
|
|
1598
|
-
for (const name of Object.keys(sheet.keyframes)) {
|
|
1701
|
+
for (const name of Object.keys(sheet.keyframes ?? {})) {
|
|
1599
1702
|
this.keyframes.set(name, sheet.keyframes[name]);
|
|
1600
1703
|
}
|
|
1704
|
+
if (sheet.fonts && this.fontFaces.add(sheet.fonts))
|
|
1705
|
+
this.facesAdded = true;
|
|
1706
|
+
}
|
|
1707
|
+
/** Every `@font-face` seen, global as on the web: a face declared in one sheet serves all. */
|
|
1708
|
+
fontFaces = new FontFaces();
|
|
1709
|
+
/** Whether a sheet first used in this commit declared a face. See `rematchFonts`. */
|
|
1710
|
+
facesAdded = false;
|
|
1711
|
+
/**
|
|
1712
|
+
* Faces a sheet declared partway through a commit: text resolved before it, in this commit or
|
|
1713
|
+
* an earlier one, matched without them. Resolve every text naming a family again, and commit.
|
|
1714
|
+
* Once per sheet that declares faces, so walking the tree costs nothing that matters.
|
|
1715
|
+
*/
|
|
1716
|
+
rematchFonts() {
|
|
1717
|
+
const visit = (node) => {
|
|
1718
|
+
for (const child of node.children) {
|
|
1719
|
+
if (child.kind !== 'element')
|
|
1720
|
+
continue;
|
|
1721
|
+
if (typeof child.committed?.props['fontFamily'] === 'string')
|
|
1722
|
+
this.markProps(child, false);
|
|
1723
|
+
visit(child);
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
visit(this.root);
|
|
1727
|
+
this.commit();
|
|
1601
1728
|
}
|
|
1602
1729
|
knownSheets = new WeakSet();
|
|
1603
1730
|
/**
|
|
@@ -2000,7 +2127,7 @@ export class Engine {
|
|
|
2000
2127
|
// is here only because something beneath it moved has the props it had.
|
|
2001
2128
|
const propsMoved = node.propsDirty || styleChanged;
|
|
2002
2129
|
const props = propsMoved ? this.mergeProps(node, viewName) : previous.props;
|
|
2003
|
-
const propsPayload = propsMoved ?
|
|
2130
|
+
const propsPayload = propsMoved ? this.propsPayload(node, previous.props, props) : null;
|
|
2004
2131
|
const childrenChanged = this.childrenMoved(node, childHandles, previous.childHandles);
|
|
2005
2132
|
let handle = previous.handle;
|
|
2006
2133
|
if (childrenChanged || propsPayload) {
|
|
@@ -2012,6 +2139,14 @@ export class Engine {
|
|
|
2012
2139
|
this.clearFlags(node);
|
|
2013
2140
|
return handle;
|
|
2014
2141
|
}
|
|
2142
|
+
/** What a clone sends: the props that changed, and a text input's font once it registered. */
|
|
2143
|
+
propsPayload(node, previous, props) {
|
|
2144
|
+
const payload = diffProps(previous, props);
|
|
2145
|
+
if (payload && this.fontsRefreshed && this.fontResends.delete(node)) {
|
|
2146
|
+
payload['fontFamily'] = props['fontFamily'];
|
|
2147
|
+
}
|
|
2148
|
+
return payload;
|
|
2149
|
+
}
|
|
2015
2150
|
/**
|
|
2016
2151
|
* Whether a node has to be committed with its children again.
|
|
2017
2152
|
*
|