import React, { forwardRef, type Ref, useEffect } from 'react'; import { Platform, Text, type TextProps, type TextStyle } from 'react-native'; import { createIconSourceCache, type ImageResult } from './create-icon-source-cache'; import { DEFAULT_ICON_COLOR, DEFAULT_ICON_SIZE } from './defaults'; import { dynamicLoader } from './dynamicLoading/dynamic-font-loading'; import { isDynamicLoadingEnabled } from './dynamicLoading/dynamic-loading-setting'; import type { FontSource } from './dynamicLoading/types'; import { type GetImageSourceOptions, getImageSource as getImageSourceImpl, getImageSourceSync as getImageSourceSyncImpl, } from './get-image-source'; type GetImageSourceSyncIconFunc = { (name: GM, options: GetImageSourceOptions): ImageResult; (name: GM, size?: number, color?: TextStyle['color']): ImageResult; }; type GetImageSourceIconFunc = { (name: GM, options: GetImageSourceOptions): Promise; (name: GM, size?: number, color?: TextStyle['color']): Promise; }; export type IconProps = TextProps & { name: T; size?: number; color?: TextStyle['color']; innerRef?: Ref; }; type GlyphMap = Record; export type IconComponent = React.FC< TextProps & { name: keyof GM; size?: number; color?: TextStyle['color']; innerRef?: Ref; } & React.RefAttributes > & { getImageSource: GetImageSourceIconFunc; getImageSourceSync: GetImageSourceSyncIconFunc; }; export type CreateIconSetOptions = { postScriptName: string; fontFileName: string; fontSource?: FontSource; fontStyle?: TextProps['style']; }; export function createIconSet( glyphMap: GM, postScriptName: string, fontFileName: string, fontStyle?: TextProps['style'], ): IconComponent; export function createIconSet(glyphMap: GM, options: CreateIconSetOptions): IconComponent; export function createIconSet( glyphMap: GM, postScriptNameOrOptions: string | CreateIconSetOptions, fontFileNameParam?: string, fontStyleParam?: TextProps['style'], ): IconComponent { const { postScriptName, fontFileName, fontStyle } = typeof postScriptNameOrOptions === 'string' ? { postScriptName: postScriptNameOrOptions, fontFileName: fontFileNameParam, fontStyle: fontStyleParam, } : postScriptNameOrOptions; const fontBasename = fontFileName ? fontFileName.replace(/\.(otf|ttf)$/, '') : postScriptName; const fontReference = Platform.select({ windows: `/Assets/${fontFileName}#${postScriptName}`, android: fontBasename, default: postScriptName, }); const styleOverrides: TextProps['style'] = { fontFamily: fontReference, fontWeight: 'normal', fontStyle: 'normal', }; const fontSource = typeof postScriptNameOrOptions === 'object' && postScriptNameOrOptions?.fontSource; const resolveGlyph = (name: keyof GM): string => { const glyph = glyphMap[name] || '?'; if (typeof glyph === 'number') { return String.fromCodePoint(glyph); } return glyph; }; const Icon = ({ name, size = DEFAULT_ICON_SIZE, color = DEFAULT_ICON_COLOR, style, children, allowFontScaling = false, innerRef, ...props }: IconProps) => { const canUseDynamicLoading = !!fontSource && isDynamicLoadingEnabled(); const [isFontLoaded, setIsFontLoaded] = React.useState( canUseDynamicLoading ? dynamicLoader.isLoaded(fontReference) : true, ); const glyph = isFontLoaded && name ? resolveGlyph(name) : ''; const shouldLoadFontDynamically = !isFontLoaded && canUseDynamicLoading; // biome-ignore lint/correctness/useExhaustiveDependencies: the dependencies never change useEffect(() => { let isMounted = true; if (shouldLoadFontDynamically) { dynamicLoader.loadFontAsync(fontReference, fontSource).finally(() => { if (isMounted) { setIsFontLoaded(true); } }); } return () => { isMounted = false; }; }, []); const styleDefaults = { fontSize: size, color, }; const newProps: TextProps = { ...props, style: [styleDefaults, style, styleOverrides, fontStyle], allowFontScaling, }; return ( {glyph} {children} ); }; const WrappedIcon = forwardRef>((props, ref) => ( )); WrappedIcon.displayName = `Icon(${postScriptName})`; const imageSourceCache = createIconSourceCache(); const getImageSource: GetImageSourceIconFunc = async ( name: keyof GM, sizeOrOptions?: number | GetImageSourceOptions, color?: TextStyle['color'], ) => { const canUseDynamicLoading = !!fontSource && isDynamicLoadingEnabled(); if (canUseDynamicLoading && !dynamicLoader.isLoaded(fontReference)) { await dynamicLoader.loadFontAsync(fontReference, fontSource); } const options = typeof sizeOrOptions === 'object' ? sizeOrOptions : { size: sizeOrOptions, color }; return getImageSourceImpl(imageSourceCache, fontReference, resolveGlyph(name), options); }; const getImageSourceSync: GetImageSourceSyncIconFunc = ( name: keyof GM, sizeOrOptions?: number | GetImageSourceOptions, color?: TextStyle['color'], ) => { const options = typeof sizeOrOptions === 'object' ? sizeOrOptions : { size: sizeOrOptions, color }; return getImageSourceSyncImpl(imageSourceCache, fontReference, resolveGlyph(name), options); }; const IconNamespace = Object.assign(WrappedIcon, { getImageSource, getImageSourceSync, }); return IconNamespace; }