@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/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
- /** Take a sheet's `@keyframes` into the registry. Cheap and idempotent; sheets are few. */
685
- private registerKeyframes;
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.registerKeyframes(options.globalStyles);
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.registerKeyframes(node.sheet);
1537
- this.registerKeyframes(node.hostSheet);
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
- /** Take a sheet's `@keyframes` into the registry. Cheap and idempotent; sheets are few. */
1594
- registerKeyframes(sheet) {
1595
- if (!sheet?.keyframes || this.knownSheets.has(sheet))
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 ? diffProps(previous.props, props) : null;
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
  *