react-native-nano-icons 0.1.8 → 0.2.1
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 +64 -2
- package/android/src/main/java/com/nanoicons/NanoIconsFontLoaderModule.kt +63 -0
- package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +19 -2
- package/ios/NanoIconView.h +5 -0
- package/ios/NanoIconView.mm +35 -9
- package/ios/NanoIconsFontLoader.h +11 -0
- package/ios/NanoIconsFontLoader.mm +110 -0
- package/lib/commonjs/cli/build.d.ts +3 -0
- package/lib/commonjs/cli/build.js +8 -4
- package/lib/commonjs/cli/config.d.ts +1 -0
- package/lib/commonjs/cli/config.js +10 -0
- package/lib/commonjs/cli/expoConfig.d.ts +8 -0
- package/lib/commonjs/cli/expoConfig.js +34 -0
- package/lib/commonjs/cli/index.d.ts +2 -1
- package/lib/commonjs/cli/index.js +4 -1
- package/lib/commonjs/cli/link.js +32 -22
- package/lib/commonjs/plugin/src/types.d.ts +9 -0
- package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +5 -0
- package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +24 -10
- package/lib/commonjs/scripts/cli.js +23 -11
- package/lib/commonjs/src/core/pipeline/config.d.ts +1 -0
- package/lib/commonjs/src/core/pipeline/managers.js +1 -2
- package/lib/commonjs/src/core/pipeline/run.js +1 -0
- package/lib/commonjs/src/core/svg/svg_dom.d.ts +1 -0
- package/lib/commonjs/src/core/svg/svg_dom.js +22 -17
- package/lib/commonjs/src/core/svg/svg_pathops.js +30 -16
- package/lib/commonjs/src/core/types.d.ts +3 -0
- package/lib/module/const/codegenPrimitives.js +2 -0
- package/lib/module/const/codegenPrimitives.js.map +1 -0
- package/lib/module/core/font/compile.js.map +1 -1
- package/lib/module/core/pipeline/config.js.map +1 -1
- package/lib/module/core/pipeline/managers.js.map +1 -1
- package/lib/module/core/pipeline/run.js +4 -1
- package/lib/module/core/pipeline/run.js.map +1 -1
- package/lib/module/core/svg/layers.js.map +1 -1
- package/lib/module/core/svg/svg_dom.js +19 -19
- package/lib/module/core/svg/svg_dom.js.map +1 -1
- package/lib/module/core/svg/svg_pathops.js.map +1 -1
- package/lib/module/createNanoIconsSet.js +2 -2
- package/lib/module/createNanoIconsSet.js.map +1 -1
- package/lib/module/createNanoIconsSet.native.js +43 -16
- package/lib/module/createNanoIconsSet.native.js.map +1 -1
- package/lib/module/createNanoIconsSet.shared.js +49 -20
- package/lib/module/createNanoIconsSet.shared.js.map +1 -1
- package/lib/module/createNanoIconsSet.web.js +78 -0
- package/lib/module/createNanoIconsSet.web.js.map +1 -0
- package/lib/module/loadDynamicFont.js +118 -0
- package/lib/module/loadDynamicFont.js.map +1 -0
- package/lib/module/specs/NanoIconViewNativeComponent.ts +9 -8
- package/lib/module/specs/NativeNanoIconsFontLoader.js +7 -0
- package/lib/module/specs/NativeNanoIconsFontLoader.js.map +1 -0
- package/lib/module/utils/glyphRuntime.js +37 -0
- package/lib/module/utils/glyphRuntime.js.map +1 -0
- package/lib/typescript/__tests__/glyphRuntime.unit.test.d.ts +2 -0
- package/lib/typescript/__tests__/glyphRuntime.unit.test.d.ts.map +1 -0
- package/lib/typescript/__tests__/link.unit.test.d.ts +3 -0
- package/lib/typescript/__tests__/link.unit.test.d.ts.map +1 -0
- package/lib/typescript/__tests__/loadDynamicFont.unit.test.d.ts +2 -0
- package/lib/typescript/__tests__/loadDynamicFont.unit.test.d.ts.map +1 -0
- package/lib/typescript/cli/build.d.ts +3 -0
- package/lib/typescript/cli/build.d.ts.map +1 -1
- package/lib/typescript/cli/link.d.ts +12 -0
- package/lib/typescript/cli/link.d.ts.map +1 -0
- package/lib/typescript/src/const/codegenPrimitives.d.ts +3 -0
- package/lib/typescript/src/const/codegenPrimitives.d.ts.map +1 -0
- package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/config.d.ts +1 -0
- package/lib/typescript/src/core/pipeline/config.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
- package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts +1 -0
- package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +3 -0
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.d.ts +1 -0
- package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.native.d.ts +1 -0
- package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.web.d.ts +7 -0
- package/lib/typescript/src/createNanoIconsSet.web.d.ts.map +1 -0
- package/lib/typescript/src/loadDynamicFont.d.ts +28 -0
- package/lib/typescript/src/loadDynamicFont.d.ts.map +1 -0
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +9 -8
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeNanoIconsFontLoader.d.ts +8 -0
- package/lib/typescript/src/specs/NativeNanoIconsFontLoader.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +7 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/glyphRuntime.d.ts +18 -0
- package/lib/typescript/src/utils/glyphRuntime.d.ts.map +1 -0
- package/package.json +8 -5
- package/plugin/src/types.ts +9 -0
- package/plugin/src/withNanoIconsFontLinking.ts +29 -10
- package/react-native-nano-icons.podspec +1 -1
- package/scripts/cli.ts +31 -11
- package/src/const/codegenPrimitives.ts +14 -0
- package/src/core/font/compile.ts +1 -2
- package/src/core/pipeline/config.ts +1 -0
- package/src/core/pipeline/managers.ts +5 -10
- package/src/core/pipeline/run.ts +11 -6
- package/src/core/svg/layers.ts +4 -4
- package/src/core/svg/svg_dom.ts +26 -23
- package/src/core/svg/svg_pathops.ts +50 -24
- package/src/core/types.ts +10 -2
- package/src/createNanoIconsSet.native.tsx +78 -26
- package/src/createNanoIconsSet.shared.tsx +93 -40
- package/src/createNanoIconsSet.tsx +9 -1
- package/src/createNanoIconsSet.web.tsx +109 -0
- package/src/loadDynamicFont.ts +162 -0
- package/src/specs/NanoIconViewNativeComponent.ts +9 -8
- package/src/specs/NativeNanoIconsFontLoader.ts +11 -0
- package/src/types.ts +5 -1
- package/src/utils/glyphRuntime.ts +46 -0
|
@@ -457,7 +457,6 @@ function bestStartMinYMinX(
|
|
|
457
457
|
return best;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
|
|
461
460
|
// ---------------------------------------------------------------------------
|
|
462
461
|
// Containment helpers (module-level for reuse by fixPathWinding)
|
|
463
462
|
// ---------------------------------------------------------------------------
|
|
@@ -510,7 +509,10 @@ function contourToPolyline(
|
|
|
510
509
|
for (let i = 1; i <= steps; i++) {
|
|
511
510
|
const t = i / steps;
|
|
512
511
|
const mt = 1 - t;
|
|
513
|
-
pts.push([
|
|
512
|
+
pts.push([
|
|
513
|
+
mt * mt * x0 + 2 * mt * t * x1 + t * t * x2,
|
|
514
|
+
mt * mt * y0 + 2 * mt * t * y1 + t * t * y2,
|
|
515
|
+
]);
|
|
514
516
|
}
|
|
515
517
|
cx = x2;
|
|
516
518
|
cy = y2;
|
|
@@ -527,8 +529,14 @@ function contourToPolyline(
|
|
|
527
529
|
const t = i / steps;
|
|
528
530
|
const mt = 1 - t;
|
|
529
531
|
pts.push([
|
|
530
|
-
mt * mt * mt * x0 +
|
|
531
|
-
|
|
532
|
+
mt * mt * mt * x0 +
|
|
533
|
+
3 * mt * mt * t * x1 +
|
|
534
|
+
3 * mt * t * t * x2 +
|
|
535
|
+
t * t * t * x3,
|
|
536
|
+
mt * mt * mt * y0 +
|
|
537
|
+
3 * mt * mt * t * y1 +
|
|
538
|
+
3 * mt * t * t * y2 +
|
|
539
|
+
t * t * t * y3,
|
|
532
540
|
]);
|
|
533
541
|
}
|
|
534
542
|
cx = x3;
|
|
@@ -550,10 +558,7 @@ function pointInPolygon(px: number, py: number, poly: Point[]): boolean {
|
|
|
550
558
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
551
559
|
const [xi, yi] = poly[i]!;
|
|
552
560
|
const [xj, yj] = poly[j]!;
|
|
553
|
-
if (
|
|
554
|
-
yi > py !== yj > py &&
|
|
555
|
-
px < ((xj - xi) * (py - yi)) / (yj - yi) + xi
|
|
556
|
-
) {
|
|
561
|
+
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) {
|
|
557
562
|
inside = !inside;
|
|
558
563
|
}
|
|
559
564
|
}
|
|
@@ -563,7 +568,10 @@ function pointInPolygon(px: number, py: number, poly: Point[]): boolean {
|
|
|
563
568
|
/**
|
|
564
569
|
* Get a representative point on the contour boundary (midpoint of first segment).
|
|
565
570
|
*/
|
|
566
|
-
function getContourSamplePoint(
|
|
571
|
+
function getContourSamplePoint(
|
|
572
|
+
contourCmds: readonly Cmd[],
|
|
573
|
+
V: VerbMap
|
|
574
|
+
): Point | null {
|
|
567
575
|
let cx = 0,
|
|
568
576
|
cy = 0;
|
|
569
577
|
for (const cmd of contourCmds) {
|
|
@@ -584,8 +592,14 @@ function getContourSamplePoint(contourCmds: readonly Cmd[], V: VerbMap): Point |
|
|
|
584
592
|
const t = 0.5,
|
|
585
593
|
mt = 0.5;
|
|
586
594
|
return [
|
|
587
|
-
mt ** 3 * cx +
|
|
588
|
-
|
|
595
|
+
mt ** 3 * cx +
|
|
596
|
+
3 * mt ** 2 * t * cmd[1]! +
|
|
597
|
+
3 * mt * t ** 2 * cmd[3]! +
|
|
598
|
+
t ** 3 * cmd[5]!,
|
|
599
|
+
mt ** 3 * cy +
|
|
600
|
+
3 * mt ** 2 * t * cmd[2]! +
|
|
601
|
+
3 * mt * t ** 2 * cmd[4]! +
|
|
602
|
+
t ** 3 * cmd[6]!,
|
|
589
603
|
];
|
|
590
604
|
}
|
|
591
605
|
}
|
|
@@ -597,14 +611,20 @@ function getContourSamplePoint(contourCmds: readonly Cmd[], V: VerbMap): Point |
|
|
|
597
611
|
* Even nesting depth = CCW (outer), odd = CW (hole).
|
|
598
612
|
*/
|
|
599
613
|
function applyContainmentWinding(
|
|
600
|
-
contourObjs: Array<{
|
|
614
|
+
contourObjs: Array<{
|
|
615
|
+
cmds: Cmd[];
|
|
616
|
+
explicitCloseWanted: boolean;
|
|
617
|
+
absA: number;
|
|
618
|
+
}>,
|
|
601
619
|
V: VerbMap
|
|
602
620
|
): void {
|
|
603
621
|
const n = contourObjs.length;
|
|
604
622
|
if (n === 0) return;
|
|
605
623
|
|
|
606
624
|
const polylines = contourObjs.map((obj) => contourToPolyline(obj.cmds, V));
|
|
607
|
-
const samplePts = contourObjs.map((obj) =>
|
|
625
|
+
const samplePts = contourObjs.map((obj) =>
|
|
626
|
+
getContourSamplePoint(obj.cmds, V)
|
|
627
|
+
);
|
|
608
628
|
const depths = new Array<number>(n).fill(0);
|
|
609
629
|
|
|
610
630
|
for (let i = 0; i < n; i++) {
|
|
@@ -625,7 +645,11 @@ function applyContainmentWinding(
|
|
|
625
645
|
const a = approxSignedAreaFromContourCmds(obj.cmds, V);
|
|
626
646
|
const isCCW = a > 0;
|
|
627
647
|
if (wantCCW !== isCCW) {
|
|
628
|
-
obj.cmds = reverseClosedContourKeepStart(
|
|
648
|
+
obj.cmds = reverseClosedContourKeepStart(
|
|
649
|
+
obj.cmds,
|
|
650
|
+
obj.explicitCloseWanted,
|
|
651
|
+
V
|
|
652
|
+
);
|
|
629
653
|
}
|
|
630
654
|
};
|
|
631
655
|
|
|
@@ -692,7 +716,9 @@ export function convertEvenoddToWinding(
|
|
|
692
716
|
if (v === V.MOVE) parts.push(`M${roundN(cmd[1]!)} ${roundN(cmd[2]!)}`);
|
|
693
717
|
else if (v === V.LINE) parts.push(`L${roundN(cmd[1]!)} ${roundN(cmd[2]!)}`);
|
|
694
718
|
else if (v === V.QUAD)
|
|
695
|
-
parts.push(
|
|
719
|
+
parts.push(
|
|
720
|
+
`Q${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)}`
|
|
721
|
+
);
|
|
696
722
|
else if (v === V.CUBIC)
|
|
697
723
|
parts.push(
|
|
698
724
|
`C${roundN(cmd[1]!)} ${roundN(cmd[2]!)} ${roundN(cmd[3]!)} ${roundN(cmd[4]!)} ${roundN(cmd[5]!)} ${roundN(cmd[6]!)}`
|
|
@@ -882,17 +908,17 @@ export function buildPathopsBackend(PathKit: PathKitModule) {
|
|
|
882
908
|
|
|
883
909
|
const cap =
|
|
884
910
|
capInt === 1
|
|
885
|
-
? Caps.ROUND ?? 1
|
|
911
|
+
? (Caps.ROUND ?? 1)
|
|
886
912
|
: capInt === 2
|
|
887
|
-
|
|
888
|
-
|
|
913
|
+
? (Caps.SQUARE ?? 2)
|
|
914
|
+
: (Caps.BUTT ?? 0);
|
|
889
915
|
|
|
890
916
|
const join =
|
|
891
917
|
joinInt === 1
|
|
892
|
-
? Joins.ROUND ?? 1
|
|
918
|
+
? (Joins.ROUND ?? 1)
|
|
893
919
|
: joinInt === 2
|
|
894
|
-
|
|
895
|
-
|
|
920
|
+
? (Joins.BEVEL ?? 2)
|
|
921
|
+
: (Joins.MITER ?? 0);
|
|
896
922
|
|
|
897
923
|
const work = PathKit.NewPath(h.p);
|
|
898
924
|
|
|
@@ -961,10 +987,10 @@ export function buildPathopsBackend(PathKit: PathKitModule) {
|
|
|
961
987
|
const Ops = PathKit.PathOp ?? {};
|
|
962
988
|
const op =
|
|
963
989
|
opInt === 1
|
|
964
|
-
? Ops.INTERSECT ?? 1
|
|
990
|
+
? (Ops.INTERSECT ?? 1)
|
|
965
991
|
: opInt === 2
|
|
966
|
-
|
|
967
|
-
|
|
992
|
+
? (Ops.DIFFERENCE ?? 2)
|
|
993
|
+
: (Ops.UNION ?? 0);
|
|
968
994
|
|
|
969
995
|
const out = PathKit.MakeFromOp(aHandle.p, bHandle.p, op);
|
|
970
996
|
if (!out) return null;
|
package/src/core/types.ts
CHANGED
|
@@ -134,16 +134,24 @@ export type IconsMap = Record<string, GlyphEntry>;
|
|
|
134
134
|
* z - safe zone,
|
|
135
135
|
* s - start unicode,
|
|
136
136
|
* h - hash,
|
|
137
|
+
* l - linking mode: 's' (static, bundled — default when absent) or 'd' (dynamic, OTA-delivered),
|
|
137
138
|
* i - icons,
|
|
138
139
|
* adv - advance width,
|
|
139
140
|
*/
|
|
140
141
|
export type NanoGlyphMap = {
|
|
141
|
-
m: { f: string; u: number; z: number; s: number; h?: string };
|
|
142
|
+
m: { f: string; u: number; z: number; s: number; h?: string; l?: 's' | 'd' };
|
|
142
143
|
i: IconsMap;
|
|
143
144
|
};
|
|
144
145
|
|
|
145
146
|
/** Accepts JSON-inferred types where arrays aren't tuples. */
|
|
146
147
|
export type NanoGlyphMapInput = {
|
|
147
|
-
m: {
|
|
148
|
+
m: {
|
|
149
|
+
f: string;
|
|
150
|
+
u: number;
|
|
151
|
+
z: number;
|
|
152
|
+
s: number;
|
|
153
|
+
h?: string;
|
|
154
|
+
l?: 's' | 'd' | (string & {});
|
|
155
|
+
};
|
|
148
156
|
i: Record<string, readonly unknown[]>;
|
|
149
157
|
};
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { memo, useMemo } from 'react';
|
|
2
|
-
import { PixelRatio, UIManager, processColor } from 'react-native';
|
|
2
|
+
import { PixelRatio, UIManager, View, processColor } from 'react-native';
|
|
3
3
|
import type { NanoGlyphMapInput, GlyphEntry } from './core/types';
|
|
4
4
|
import type { IconComponent, IconProps } from './types';
|
|
5
5
|
import { shallowEqualColor } from './utils/shallowEqualColor';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ICON_SIZE,
|
|
8
|
+
resolveGlyphEntry,
|
|
9
|
+
createLayerColorResolver,
|
|
10
|
+
} from './utils/glyphRuntime';
|
|
6
11
|
import NanoIconViewNative from './specs/NanoIconViewNativeComponent';
|
|
7
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createJSIconSet,
|
|
14
|
+
warnIfLinkingMismatch,
|
|
15
|
+
} from './createNanoIconsSet.shared';
|
|
16
|
+
import { loadDynamicFont, useDynamicFontPending } from './loadDynamicFont';
|
|
8
17
|
|
|
9
18
|
export type { IconComponent, IconProps };
|
|
10
19
|
export { shallowEqualColor };
|
|
11
20
|
|
|
12
|
-
const DEFAULT_ICON_SIZE = 12;
|
|
13
|
-
|
|
14
21
|
const HAS_NATIVE_IMPL = UIManager.hasViewManagerConfig('NanoIconView');
|
|
15
22
|
|
|
16
23
|
// Shared processColor cache — avoids redundant color parsing for repeated
|
|
@@ -27,27 +34,44 @@ function cachedProcessColor(color: string): number {
|
|
|
27
34
|
|
|
28
35
|
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
29
36
|
glyphMap: GM
|
|
37
|
+
): IconComponent<GM>;
|
|
38
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
39
|
+
glyphMap: GM,
|
|
40
|
+
font: unknown
|
|
41
|
+
): IconComponent<GM>;
|
|
42
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
43
|
+
glyphMap: GM,
|
|
44
|
+
font?: unknown
|
|
30
45
|
): IconComponent<GM> {
|
|
31
46
|
if (!HAS_NATIVE_IMPL) {
|
|
32
|
-
return createJSIconSet(glyphMap);
|
|
47
|
+
return createJSIconSet(glyphMap, font);
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
const fontFamilyBasename = glyphMap.m.f;
|
|
36
51
|
const unitsPerEm = glyphMap.m.u;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
warnIfLinkingMismatch(fontFamilyBasename, glyphMap.m.l, font);
|
|
53
|
+
|
|
54
|
+
// dynamically linked font - register and hide icons until ready
|
|
55
|
+
const managed = glyphMap.m.l === 'd' && font != null;
|
|
56
|
+
if (managed) {
|
|
57
|
+
void loadDynamicFont(fontFamilyBasename, font).catch((err) => {
|
|
58
|
+
if (__DEV__)
|
|
59
|
+
console.warn(
|
|
60
|
+
`[react-native-nano-icons] Failed to load dynamic font "${fontFamilyBasename}".`,
|
|
61
|
+
err
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
44
65
|
|
|
45
66
|
// Pre-compute per-icon static data (codepoints, default colors) once at set creation
|
|
46
67
|
// Avoids layers.map() + processColor per icon mount
|
|
47
68
|
const codepointsCache = new Map<string, readonly number[]>();
|
|
48
69
|
const defaultColorsCache = new Map<string, readonly number[]>();
|
|
49
70
|
|
|
50
|
-
function getCodepoints(
|
|
71
|
+
function getCodepoints(
|
|
72
|
+
name: string,
|
|
73
|
+
layers: GlyphEntry[1]
|
|
74
|
+
): readonly number[] {
|
|
51
75
|
let cp = codepointsCache.get(name);
|
|
52
76
|
if (!cp) {
|
|
53
77
|
cp = layers.map(([c]) => c);
|
|
@@ -56,10 +80,15 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
|
56
80
|
return cp;
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
function getDefaultColors(
|
|
83
|
+
function getDefaultColors(
|
|
84
|
+
name: string,
|
|
85
|
+
layers: GlyphEntry[1]
|
|
86
|
+
): readonly number[] {
|
|
60
87
|
let colors = defaultColorsCache.get(name);
|
|
61
88
|
if (!colors) {
|
|
62
|
-
colors = layers.map(([, srcColor]) =>
|
|
89
|
+
colors = layers.map(([, srcColor]) =>
|
|
90
|
+
cachedProcessColor(srcColor ?? 'black')
|
|
91
|
+
);
|
|
63
92
|
defaultColorsCache.set(name, colors);
|
|
64
93
|
}
|
|
65
94
|
return colors;
|
|
@@ -75,14 +104,18 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
|
75
104
|
accessible,
|
|
76
105
|
accessibilityLabel,
|
|
77
106
|
accessibilityRole = 'image',
|
|
107
|
+
accessibilityElementsHidden,
|
|
108
|
+
importantForAccessibility,
|
|
78
109
|
testID,
|
|
79
110
|
ref,
|
|
80
111
|
}: IconProps<keyof GM['i']>) => {
|
|
81
112
|
const fontScale = allowFontScaling ? PixelRatio.getFontScale() : 1;
|
|
82
|
-
const [adv, layers] =
|
|
113
|
+
const [adv, layers] = resolveGlyphEntry(glyphMap, name);
|
|
83
114
|
const scaledSize = size * fontScale;
|
|
84
115
|
const width = (adv / unitsPerEm) * scaledSize;
|
|
85
116
|
|
|
117
|
+
const pending = useDynamicFontPending(managed, fontFamilyBasename);
|
|
118
|
+
|
|
86
119
|
const nameStr = name as string;
|
|
87
120
|
const codepoints = getCodepoints(nameStr, layers);
|
|
88
121
|
|
|
@@ -91,15 +124,10 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
|
91
124
|
if (color === undefined || color === null) {
|
|
92
125
|
return getDefaultColors(nameStr, layers);
|
|
93
126
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return layers.map(([, srcColor], i) => {
|
|
99
|
-
const layerColor =
|
|
100
|
-
colorArray[i] ?? lastPaletteColor ?? srcColor ?? 'black';
|
|
101
|
-
return cachedProcessColor(layerColor as string);
|
|
102
|
-
});
|
|
127
|
+
const resolveColor = createLayerColorResolver(color);
|
|
128
|
+
return layers.map(([, srcColor], i) =>
|
|
129
|
+
cachedProcessColor(resolveColor(i, srcColor) as string)
|
|
130
|
+
);
|
|
103
131
|
}, [nameStr, color]);
|
|
104
132
|
|
|
105
133
|
const nativeStyle = useMemo(
|
|
@@ -107,6 +135,23 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
|
107
135
|
[scaledSize, width, style]
|
|
108
136
|
);
|
|
109
137
|
|
|
138
|
+
// Hide-until-ready: while the dynamic font is registering, render a
|
|
139
|
+
// placeholder. The native view mounts only once the font is registered.
|
|
140
|
+
if (pending) {
|
|
141
|
+
return (
|
|
142
|
+
<View
|
|
143
|
+
ref={ref}
|
|
144
|
+
style={nativeStyle}
|
|
145
|
+
accessible={accessible}
|
|
146
|
+
accessibilityRole={accessibilityRole}
|
|
147
|
+
accessibilityLabel={accessibilityLabel ?? (name as string)}
|
|
148
|
+
accessibilityElementsHidden={accessibilityElementsHidden}
|
|
149
|
+
importantForAccessibility={importantForAccessibility}
|
|
150
|
+
testID={testID}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
return (
|
|
111
156
|
<NanoIconViewNative
|
|
112
157
|
ref={ref}
|
|
@@ -122,6 +167,8 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
|
122
167
|
accessible={accessible}
|
|
123
168
|
accessibilityRole={accessibilityRole}
|
|
124
169
|
accessibilityLabel={accessibilityLabel ?? (name as string)}
|
|
170
|
+
accessibilityElementsHidden={accessibilityElementsHidden}
|
|
171
|
+
importantForAccessibility={importantForAccessibility}
|
|
125
172
|
testID={testID}
|
|
126
173
|
/>
|
|
127
174
|
);
|
|
@@ -136,5 +183,10 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
|
136
183
|
|
|
137
184
|
Icon.displayName = `NanoIcon(${fontFamilyBasename})`;
|
|
138
185
|
|
|
139
|
-
|
|
186
|
+
const IconComp = Icon as unknown as IconComponent<GM>;
|
|
187
|
+
IconComp.loadFont = (override) =>
|
|
188
|
+
glyphMap.m.l === 'd'
|
|
189
|
+
? loadDynamicFont(fontFamilyBasename, override ?? font, { force: true })
|
|
190
|
+
: Promise.resolve();
|
|
191
|
+
return IconComp;
|
|
140
192
|
}
|
|
@@ -1,13 +1,52 @@
|
|
|
1
1
|
import { memo, useMemo } from 'react';
|
|
2
2
|
import { PixelRatio, Platform, Text, View, type TextProps } from 'react-native';
|
|
3
|
-
import type { NanoGlyphMapInput
|
|
3
|
+
import type { NanoGlyphMapInput } from './core/types';
|
|
4
4
|
import type { IconComponent, IconProps } from './types';
|
|
5
5
|
import { shallowEqualColor } from './utils/shallowEqualColor';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ICON_SIZE,
|
|
8
|
+
resolveGlyphEntry,
|
|
9
|
+
createCharCache,
|
|
10
|
+
createLayerColorResolver,
|
|
11
|
+
} from './utils/glyphRuntime';
|
|
12
|
+
import { loadDynamicFont, useDynamicFontPending } from './loadDynamicFont';
|
|
6
13
|
|
|
7
14
|
export type { IconComponent, IconProps };
|
|
8
15
|
export { shallowEqualColor };
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Warn when the `font` argument and the glyphmap's linking mode are inconsistent.
|
|
19
|
+
*
|
|
20
|
+
* - Dynamic glyphmap without a `font` arg: caller probably forgot to pass it; icons
|
|
21
|
+
* will render as tofu until the host app loads a font under family `fontFamily`.
|
|
22
|
+
* - Static glyphmap with a `font` arg: argument is ignored, font is bundled natively.
|
|
23
|
+
*
|
|
24
|
+
* Fires once per `createIconSet` call (not per render).
|
|
25
|
+
*/
|
|
26
|
+
export function warnIfLinkingMismatch(
|
|
27
|
+
fontFamily: string,
|
|
28
|
+
linking: string | undefined,
|
|
29
|
+
font: unknown
|
|
30
|
+
): void {
|
|
31
|
+
if (!__DEV__) return;
|
|
32
|
+
|
|
33
|
+
const isDynamic = linking === 'd';
|
|
34
|
+
if (isDynamic && font == null) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[react-native-nano-icons] "${fontFamily}" is built with dynamic linking ` +
|
|
37
|
+
`but no font was passed to createIconSet. Icons will render as tofu ` +
|
|
38
|
+
`until a font is loaded and registered under family "${fontFamily}".`
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!isDynamic && font != null) {
|
|
43
|
+
console.warn(
|
|
44
|
+
`[react-native-nano-icons] "${fontFamily}" is built with static linking; ` +
|
|
45
|
+
`the font argument passed to createIconSet is ignored. ` +
|
|
46
|
+
`Set linking: 'dynamic' in your config to opt into OTA delivery.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
11
50
|
|
|
12
51
|
/**
|
|
13
52
|
* JS implementation using <View> + <Text> layers.
|
|
@@ -15,8 +54,29 @@ const DEFAULT_ICON_SIZE = 12;
|
|
|
15
54
|
*/
|
|
16
55
|
export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
17
56
|
glyphMap: GM
|
|
57
|
+
): IconComponent<GM>;
|
|
58
|
+
export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
59
|
+
glyphMap: GM,
|
|
60
|
+
font: unknown
|
|
61
|
+
): IconComponent<GM>;
|
|
62
|
+
export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
63
|
+
glyphMap: GM,
|
|
64
|
+
font?: unknown
|
|
18
65
|
): IconComponent<GM> {
|
|
19
66
|
const fontBasename = glyphMap.m.f;
|
|
67
|
+
warnIfLinkingMismatch(fontBasename, glyphMap.m.l, font);
|
|
68
|
+
|
|
69
|
+
// dynamically linked font - register and hide icons until ready
|
|
70
|
+
const managed = glyphMap.m.l === 'd' && font != null;
|
|
71
|
+
if (managed) {
|
|
72
|
+
void loadDynamicFont(fontBasename, font).catch((err) => {
|
|
73
|
+
if (__DEV__)
|
|
74
|
+
console.warn(
|
|
75
|
+
`[react-native-nano-icons] Failed to load dynamic font "${fontBasename}".`,
|
|
76
|
+
err
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
20
80
|
|
|
21
81
|
const fontReference = Platform.select({
|
|
22
82
|
windows: `/Assets/${fontBasename}`,
|
|
@@ -33,23 +93,7 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
|
33
93
|
};
|
|
34
94
|
|
|
35
95
|
const unitsPerEm = glyphMap.m.u;
|
|
36
|
-
|
|
37
|
-
const resolveEntry = (name: keyof GM['i']): GlyphEntry => {
|
|
38
|
-
return (glyphMap.i[name as string] ?? [
|
|
39
|
-
unitsPerEm,
|
|
40
|
-
[[63, 'black']],
|
|
41
|
-
]) as GlyphEntry;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const codepointCache = new Map<number, string>();
|
|
45
|
-
const getChar = (codepoint: number): string => {
|
|
46
|
-
let ch = codepointCache.get(codepoint);
|
|
47
|
-
if (ch === undefined) {
|
|
48
|
-
ch = String.fromCodePoint(codepoint);
|
|
49
|
-
codepointCache.set(codepoint, ch);
|
|
50
|
-
}
|
|
51
|
-
return ch;
|
|
52
|
-
};
|
|
96
|
+
const getChar = createCharCache();
|
|
53
97
|
|
|
54
98
|
const Icon = memo(
|
|
55
99
|
({
|
|
@@ -61,18 +105,19 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
|
61
105
|
accessible,
|
|
62
106
|
accessibilityLabel,
|
|
63
107
|
accessibilityRole = 'image',
|
|
108
|
+
accessibilityElementsHidden,
|
|
109
|
+
importantForAccessibility,
|
|
64
110
|
testID,
|
|
65
111
|
ref,
|
|
66
112
|
}: IconProps<keyof GM['i']>) => {
|
|
67
113
|
const fontScale = allowFontScaling ? PixelRatio.getFontScale() : 1;
|
|
68
|
-
const [adv, layers] =
|
|
114
|
+
const [adv, layers] = resolveGlyphEntry(glyphMap, name);
|
|
69
115
|
const scaledSize = size * fontScale;
|
|
70
116
|
const width = (adv / unitsPerEm) * scaledSize;
|
|
71
117
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: undefined;
|
|
118
|
+
const pending = useDynamicFontPending(managed, fontBasename);
|
|
119
|
+
|
|
120
|
+
const resolveColor = createLayerColorResolver(color);
|
|
76
121
|
|
|
77
122
|
const containerStyle = useMemo(
|
|
78
123
|
() => [{ height: scaledSize, width, bottom: 0 as const }, style],
|
|
@@ -88,22 +133,25 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
|
88
133
|
accessible={accessible}
|
|
89
134
|
accessibilityRole={accessibilityRole}
|
|
90
135
|
accessibilityLabel={accessibilityLabel ?? (name as string)}
|
|
136
|
+
accessibilityElementsHidden={accessibilityElementsHidden}
|
|
137
|
+
importantForAccessibility={importantForAccessibility}
|
|
91
138
|
testID={testID}>
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
{pending
|
|
140
|
+
? null
|
|
141
|
+
: layers.map(([codepoint, srcColor], i) => {
|
|
142
|
+
const layerColor = resolveColor(i, srcColor);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Text
|
|
146
|
+
key={i}
|
|
147
|
+
selectable={false}
|
|
148
|
+
accessible={false}
|
|
149
|
+
allowFontScaling={allowFontScaling}
|
|
150
|
+
style={[styleOverrides, sizeStyle, { color: layerColor }]}>
|
|
151
|
+
{getChar(codepoint)}
|
|
152
|
+
</Text>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
107
155
|
</View>
|
|
108
156
|
);
|
|
109
157
|
},
|
|
@@ -117,5 +165,10 @@ export function createJSIconSet<GM extends NanoGlyphMapInput>(
|
|
|
117
165
|
|
|
118
166
|
Icon.displayName = `NanoIcon(${fontBasename})`;
|
|
119
167
|
|
|
120
|
-
|
|
168
|
+
const IconComp = Icon as unknown as IconComponent<GM>;
|
|
169
|
+
IconComp.loadFont = (override) =>
|
|
170
|
+
glyphMap.m.l === 'd'
|
|
171
|
+
? loadDynamicFont(fontBasename, override ?? font, { force: true })
|
|
172
|
+
: Promise.resolve();
|
|
173
|
+
return IconComp;
|
|
121
174
|
}
|
|
@@ -7,6 +7,14 @@ export { shallowEqualColor } from './utils/shallowEqualColor';
|
|
|
7
7
|
|
|
8
8
|
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
9
9
|
glyphMap: GM
|
|
10
|
+
): IconComponent<GM>;
|
|
11
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
12
|
+
glyphMap: GM,
|
|
13
|
+
font: unknown
|
|
14
|
+
): IconComponent<GM>;
|
|
15
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
16
|
+
glyphMap: GM,
|
|
17
|
+
font?: unknown
|
|
10
18
|
): IconComponent<GM> {
|
|
11
|
-
return createJSIconSet(glyphMap);
|
|
19
|
+
return createJSIconSet(glyphMap, font);
|
|
12
20
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { memo, useMemo, type CSSProperties } from 'react';
|
|
2
|
+
import type { NanoGlyphMapInput } from './core/types';
|
|
3
|
+
import type { IconComponent, IconProps } from './types';
|
|
4
|
+
import { shallowEqualColor } from './utils/shallowEqualColor';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_ICON_SIZE,
|
|
7
|
+
resolveGlyphEntry,
|
|
8
|
+
createCharCache,
|
|
9
|
+
createLayerColorResolver,
|
|
10
|
+
} from './utils/glyphRuntime';
|
|
11
|
+
|
|
12
|
+
export type { IconComponent, IconProps };
|
|
13
|
+
export { shallowEqualColor };
|
|
14
|
+
|
|
15
|
+
// Web renderer: uses inline <span> elements so icons flow out of the box (display: inline-block keeps width/height)
|
|
16
|
+
export function createIconSet<GM extends NanoGlyphMapInput>(
|
|
17
|
+
glyphMap: GM
|
|
18
|
+
): IconComponent<GM> {
|
|
19
|
+
const fontBasename = glyphMap.m.f;
|
|
20
|
+
const unitsPerEm = glyphMap.m.u;
|
|
21
|
+
const getChar = createCharCache();
|
|
22
|
+
|
|
23
|
+
const Icon = memo(
|
|
24
|
+
({
|
|
25
|
+
name,
|
|
26
|
+
size = DEFAULT_ICON_SIZE,
|
|
27
|
+
color,
|
|
28
|
+
style,
|
|
29
|
+
accessible,
|
|
30
|
+
accessibilityLabel,
|
|
31
|
+
accessibilityRole = 'image',
|
|
32
|
+
accessibilityElementsHidden,
|
|
33
|
+
testID,
|
|
34
|
+
ref,
|
|
35
|
+
}: IconProps<keyof GM['i']>) => {
|
|
36
|
+
const [adv, layers] = resolveGlyphEntry(glyphMap, name);
|
|
37
|
+
const width = (adv / unitsPerEm) * size;
|
|
38
|
+
|
|
39
|
+
const resolveColor = createLayerColorResolver(color);
|
|
40
|
+
|
|
41
|
+
const containerStyle = useMemo<CSSProperties>(
|
|
42
|
+
() => ({
|
|
43
|
+
display: 'inline-block',
|
|
44
|
+
position: 'relative',
|
|
45
|
+
width,
|
|
46
|
+
height: size,
|
|
47
|
+
lineHeight: 0,
|
|
48
|
+
verticalAlign: 'middle',
|
|
49
|
+
...(style as CSSProperties | undefined),
|
|
50
|
+
}),
|
|
51
|
+
[size, width, style]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const layerBaseStyle = useMemo<CSSProperties>(
|
|
55
|
+
() => ({
|
|
56
|
+
position: 'absolute',
|
|
57
|
+
left: 0,
|
|
58
|
+
bottom: 0,
|
|
59
|
+
fontFamily: fontBasename,
|
|
60
|
+
fontWeight: 'normal',
|
|
61
|
+
fontStyle: 'normal',
|
|
62
|
+
fontSize: size,
|
|
63
|
+
lineHeight: 1,
|
|
64
|
+
whiteSpace: 'pre',
|
|
65
|
+
}),
|
|
66
|
+
[size]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const isHidden = accessible === false || accessibilityElementsHidden;
|
|
70
|
+
const role = accessibilityRole === 'image' ? 'img' : accessibilityRole;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<span
|
|
74
|
+
ref={ref as React.Ref<HTMLSpanElement>}
|
|
75
|
+
style={containerStyle}
|
|
76
|
+
role={isHidden ? undefined : role}
|
|
77
|
+
aria-label={
|
|
78
|
+
isHidden ? undefined : (accessibilityLabel ?? (name as string))
|
|
79
|
+
}
|
|
80
|
+
aria-hidden={isHidden || undefined}
|
|
81
|
+
data-testid={testID}>
|
|
82
|
+
{layers.map(([codepoint, srcColor], i) => {
|
|
83
|
+
const layerColor = resolveColor(i, srcColor);
|
|
84
|
+
return (
|
|
85
|
+
<span
|
|
86
|
+
key={i}
|
|
87
|
+
aria-hidden
|
|
88
|
+
style={{ ...layerBaseStyle, color: layerColor as string }}>
|
|
89
|
+
{getChar(codepoint)}
|
|
90
|
+
</span>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
(prev, next) =>
|
|
97
|
+
prev.name === next.name &&
|
|
98
|
+
prev.size === next.size &&
|
|
99
|
+
prev.style === next.style &&
|
|
100
|
+
shallowEqualColor(prev.color, next.color)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
Icon.displayName = `NanoIcon(${fontBasename})`;
|
|
104
|
+
|
|
105
|
+
// No-op on web: fonts come from CSS @font-face, nothing to load at runtime.
|
|
106
|
+
const IconComp = Icon as unknown as IconComponent<GM>;
|
|
107
|
+
IconComp.loadFont = () => Promise.resolve();
|
|
108
|
+
return IconComp;
|
|
109
|
+
}
|