react-native-nano-icons 0.1.3 → 0.1.5

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.
@@ -1,5 +1,4 @@
1
1
  #import "NanoIconView.h"
2
-
3
2
  #import <CoreText/CoreText.h>
4
3
  #import <React/RCTConversions.h>
5
4
  #import <React/RCTFabricComponentsPlugins.h>
@@ -8,239 +7,385 @@
8
7
 
9
8
  using namespace facebook::react;
10
9
 
10
+ // Forward-declare so the layer subclass can call the drawing method.
11
+ @interface NanoIconView ()
12
+ - (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds;
13
+ @end
14
+
15
+ // Lightweight sublayer for inline-in-Text icons. Provides a shifted pixel
16
+ // buffer so the icon can overflow the Yoga frame without a full UIView.
17
+ @interface NanoIconDrawingLayer : CALayer
18
+ @property (nonatomic, weak) NanoIconView *owner;
19
+ @end
20
+
21
+ @implementation NanoIconDrawingLayer
22
+ - (void)drawInContext:(CGContextRef)ctx {
23
+ [self.owner _drawIconInContext:ctx bounds:self.bounds];
24
+ }
25
+ @end
26
+
27
+ // Process-wide CTFontRef cache keyed by (fontFamily, fontSize).
28
+ // Avoids 1000× CTFontCreateWithName for identical (family, size) combos.
29
+ static CTFontRef NanoIconGetCachedFont(NSString *family, CGFloat size) {
30
+ static NSMutableDictionary *cache;
31
+ static dispatch_once_t onceToken;
32
+ dispatch_once(&onceToken, ^{ cache = [NSMutableDictionary new]; });
33
+
34
+ NSString *key = [NSString stringWithFormat:@"%@:%.1f", family, size];
35
+ id existing = cache[key];
36
+ if (existing) return (__bridge CTFontRef)existing;
37
+
38
+ CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)family, size, NULL);
39
+ if (font) cache[key] = (__bridge id)font;
40
+ return font;
41
+ }
42
+
11
43
  @implementation NanoIconView {
12
- CTFontRef _font;
44
+ CTFontRef _font; // borrowed from static cache — do NOT CFRelease
13
45
  NSString *_fontFamily;
14
46
  CGFloat _fontSize;
15
47
  std::vector<CGGlyph> _glyphs;
16
48
  std::vector<uint32_t> _colors;
17
- // Cached CGColor refs — rebuilt only when colors prop changes
18
49
  std::vector<CGColorRef> _cachedCGColors;
19
- // Cached layout metrics — rebuilt only when font or bounds change
20
50
  CGFloat _fitScale;
21
51
  CGPoint _baselinePosition;
22
52
  BOOL _metricsValid;
53
+
54
+ // Inline-in-Text detection — resolved once on first layout, cached until reparenting.
55
+ // Standalone icons (vast majority) skip the superview walk entirely after detection.
56
+ BOOL _inlineDetected;
57
+ BOOL _isInlineInText;
58
+ UIView * __weak _paragraphView;
59
+ CGFloat _cachedBaselineOffset;
60
+ BOOL _baselineOffsetValid;
61
+
62
+ // Drawing sublayer for inline icons — provides a shifted pixel buffer so the
63
+ // icon can overflow the Yoga frame. Lighter than a UIView (no responder chain,
64
+ // hit testing, or accessibility). Standalone icons draw directly via drawRect:.
65
+ CALayer *_drawingLayer;
23
66
  }
24
67
 
25
- - (instancetype)initWithFrame:(CGRect)frame
26
- {
68
+ - (instancetype)initWithFrame:(CGRect)frame {
27
69
  if (self = [super initWithFrame:frame]) {
28
70
  static const auto defaultProps = std::make_shared<const NanoIconViewProps>();
29
71
  _props = defaultProps;
30
72
  self.opaque = NO;
31
73
  self.backgroundColor = [UIColor clearColor];
74
+ self.clipsToBounds = NO;
75
+ self.contentMode = UIViewContentModeRedraw;
76
+
32
77
  _fitScale = 1.0;
33
78
  _baselinePosition = CGPointZero;
34
- _metricsValid = NO;
79
+
80
+ // _drawingLayer is created lazily only when inline in Text
35
81
  }
36
82
  return self;
37
83
  }
38
84
 
39
- + (ComponentDescriptorProvider)componentDescriptorProvider
40
- {
85
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
41
86
  return concreteComponentDescriptorProvider<NanoIconViewComponentDescriptor>();
42
87
  }
43
88
 
44
- - (void)_releaseCachedColors
45
- {
46
- for (CGColorRef c : _cachedCGColors) {
47
- CGColorRelease(c);
48
- }
49
- _cachedCGColors.clear();
50
- }
89
+ - (void)updateClippedSubviewsWithClipRect:(__unused CGRect)clipRect
90
+ relativeToView:(__unused UIView *)clipView {}
51
91
 
52
- - (void)_rebuildCachedColors
53
- {
54
- [self _releaseCachedColors];
55
- _cachedCGColors.resize(_colors.size());
56
- for (size_t i = 0; i < _colors.size(); i++) {
57
- uint32_t colorInt = _colors[i];
58
- CGFloat a = ((colorInt >> 24) & 0xFF) / 255.0;
59
- CGFloat r = ((colorInt >> 16) & 0xFF) / 255.0;
60
- CGFloat g = ((colorInt >> 8) & 0xFF) / 255.0;
61
- CGFloat b = (colorInt & 0xFF) / 255.0;
62
- _cachedCGColors[i] = CGColorCreateSRGB(r, g, b, a);
63
- }
64
- }
92
+ #pragma mark - Metrics
65
93
 
66
- - (void)_updateMetrics
67
- {
94
+ // Scale factor to fit the icon font's em square into the view height,
95
+ // and the CoreText baseline origin used for all glyph draws.
96
+ - (void)_updateMetrics {
68
97
  if (!_font) {
69
98
  _metricsValid = NO;
70
99
  return;
71
100
  }
72
-
73
101
  CGFloat ascent = CTFontGetAscent(_font);
74
102
  CGFloat descent = CTFontGetDescent(_font);
75
- CGFloat fontTotalHeight = ascent + descent;
76
- CGFloat viewHeight = self.bounds.size.height;
77
-
78
- _fitScale = (fontTotalHeight > 0) ? (viewHeight / fontTotalHeight) : 1.0;
103
+ CGFloat totalHeight = ascent + descent;
104
+ _fitScale = (totalHeight > 0) ? (self.bounds.size.height / totalHeight) : 1.0;
79
105
  _baselinePosition = CGPointMake(0, descent);
80
106
  _metricsValid = YES;
81
107
  }
82
108
 
83
- - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
84
- {
85
- const auto &oldViewProps = static_cast<const NanoIconViewProps &>(*_props);
86
- const auto &newViewProps = static_cast<const NanoIconViewProps &>(*props);
87
-
88
- // Recreate font if fontFamily or fontSize changed
89
- BOOL fontChanged = NO;
90
- if (oldViewProps.fontFamily != newViewProps.fontFamily || oldViewProps.fontSize != newViewProps.fontSize) {
91
- if (_font) {
92
- CFRelease(_font);
93
- _font = NULL;
109
+ // Detect whether this icon is inline inside a <Text> component and, if so,
110
+ // compute the baseline offset in a single pass. Deferred to first layout
111
+ // (not didMoveToSuperview) because Fabric may assemble the hierarchy bottom-up.
112
+ // For standalone icons this completes in 1-3 class name checks with no
113
+ // ObjC runtime method resolution or attributed string reads.
114
+ - (void)_detectAndCacheInlineState {
115
+ _inlineDetected = YES;
116
+ _isInlineInText = NO;
117
+ _paragraphView = nil;
118
+ _cachedBaselineOffset = 0;
119
+ _baselineOffsetValid = YES;
120
+
121
+ // RCTParagraphComponentView is the immediate or near-immediate parent
122
+ // when the icon is inside <Text>. Three levels covers all known layouts.
123
+ UIView *current = self.superview;
124
+ UIView *target = nil;
125
+ for (int i = 0; i < 3 && current; i++) {
126
+ if ([NSStringFromClass([current class]) isEqualToString:@"RCTParagraphComponentView"]) {
127
+ target = current;
128
+ break;
94
129
  }
95
- NSString *family = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
96
- _fontFamily = family;
97
- _fontSize = newViewProps.fontSize;
98
- _font = CTFontCreateWithName((__bridge CFStringRef)family, _fontSize, NULL);
99
- fontChanged = YES;
100
- _metricsValid = NO;
130
+ current = current.superview;
101
131
  }
102
132
 
103
- // Update glyphs if codepoints changed or font changed
104
- BOOL codepointsChanged = fontChanged || (oldViewProps.codepoints != newViewProps.codepoints);
105
- if (codepointsChanged && _font) {
106
- const auto &codepoints = newViewProps.codepoints;
107
- _glyphs.resize(codepoints.size());
133
+ if (!target) return;
108
134
 
109
- for (size_t i = 0; i < codepoints.size(); i++) {
110
- int32_t cp = codepoints[i];
111
- // Handle BMP and supplementary plane characters
112
- if (cp <= 0xFFFF) {
113
- UniChar ch = (UniChar)cp;
114
- CTFontGetGlyphsForCharacters(_font, &ch, &_glyphs[i], 1);
115
- } else {
116
- // Supplementary plane (private use area): use surrogate pair
117
- UniChar surrogates[2];
118
- surrogates[0] = (UniChar)(0xD800 + ((cp - 0x10000) >> 10));
119
- surrogates[1] = (UniChar)(0xDC00 + ((cp - 0x10000) & 0x3FF));
120
- CGGlyph glyphPair[2] = {0, 0};
121
- CTFontGetGlyphsForCharacters(_font, surrogates, glyphPair, 2);
122
- _glyphs[i] = glyphPair[0];
123
- }
124
- }
135
+ NSAttributedString *attrStr = nil;
136
+ if ([target respondsToSelector:@selector(attributedText)]) {
137
+ attrStr = [target performSelector:@selector(attributedText)];
125
138
  }
139
+ if (!attrStr || attrStr.length == 0) return;
126
140
 
127
- // Update colors
128
- if (oldViewProps.colors != newViewProps.colors) {
129
- const auto &colors = newViewProps.colors;
130
- _colors.resize(colors.size());
131
- for (size_t i = 0; i < colors.size(); i++) {
132
- _colors[i] = (uint32_t)colors[i];
133
- }
134
- [self _rebuildCachedColors];
135
- }
141
+ UIFont *f = [attrStr attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
142
+ if (!f) return;
136
143
 
137
- [super updateProps:props oldProps:oldProps];
138
- [self setNeedsDisplay];
144
+ _isInlineInText = YES;
145
+ _paragraphView = target;
146
+
147
+ // Derive the distance from this view's bottom edge to the text baseline.
148
+ // Respects custom RN lineHeight (mapped to NSParagraphStyle.maximumLineHeight)
149
+ // and any baseline offset applied by Fabric's text layout.
150
+ NSParagraphStyle *style = [attrStr attribute:NSParagraphStyleAttributeName
151
+ atIndex:0 effectiveRange:nil];
152
+ CGFloat lineHeight = (style && style.maximumLineHeight > 0)
153
+ ? style.maximumLineHeight : f.lineHeight;
154
+
155
+ NSNumber *bOff = [attrStr attribute:NSBaselineOffsetAttributeName
156
+ atIndex:0 effectiveRange:nil];
157
+ CGFloat baselineFromLineTop = f.ascender - (bOff ? bOff.doubleValue : 0);
158
+
159
+ CGFloat frameBottom = self.frame.origin.y + self.frame.size.height;
160
+ CGFloat posInLine = fmod(frameBottom, lineHeight);
161
+ if (posInLine < 0.01) posInLine = lineHeight;
162
+
163
+ _cachedBaselineOffset = MAX(0, posInLine - baselineFromLineTop);
139
164
  }
140
165
 
141
- - (void)layoutSubviews
142
- {
143
- [super layoutSubviews];
144
- _metricsValid = NO;
166
+ // Lazily create the drawing sublayer for inline-in-Text icons.
167
+ // The sublayer's frame is shifted upward so the icon overflows the Yoga box.
168
+ - (void)_ensureDrawingLayer {
169
+ if (_drawingLayer) return;
170
+ NanoIconDrawingLayer *layer = [NanoIconDrawingLayer layer];
171
+ layer.owner = self;
172
+ layer.opaque = NO;
173
+ layer.contentsScale = [UIScreen mainScreen].scale;
174
+ [self.layer addSublayer:layer];
175
+ _drawingLayer = layer;
145
176
  }
146
177
 
147
- - (void)drawRect:(CGRect)rect
148
- {
149
- if (!_font || _glyphs.empty()) {
150
- return;
178
+ // Re-detect inline state when the view moves to a new parent.
179
+ - (void)didMoveToSuperview {
180
+ [super didMoveToSuperview];
181
+ _inlineDetected = NO;
182
+ _baselineOffsetValid = NO;
183
+ }
184
+
185
+ // Invalidate cached offset when size changes (text relayout).
186
+ - (void)setBounds:(CGRect)bounds {
187
+ if (!CGSizeEqualToSize(self.bounds.size, bounds.size)) {
188
+ _baselineOffsetValid = NO;
151
189
  }
190
+ [super setBounds:bounds];
191
+ }
152
192
 
153
- CGContextRef context = UIGraphicsGetCurrentContext();
154
- if (!context) {
155
- return;
193
+ #pragma mark - Layout
194
+
195
+ // Standalone: no work beyond metrics validation (drawing via drawRect: on self).
196
+ // Inline: position the drawing sublayer with the cached baseline offset,
197
+ // recomputing only when bounds change or the view is reparented.
198
+ - (void)layoutSubviews {
199
+ [super layoutSubviews];
200
+ if (!_metricsValid) [self _updateMetrics];
201
+
202
+ if (!_inlineDetected) {
203
+ [self _detectAndCacheInlineState];
156
204
  }
157
205
 
158
- if (!_metricsValid) {
159
- [self _updateMetrics];
206
+ if (_isInlineInText) {
207
+ if (!_baselineOffsetValid) {
208
+ [self _detectAndCacheInlineState];
209
+ }
210
+ BOOL created = !_drawingLayer;
211
+ [self _ensureDrawingLayer];
212
+ CGRect newFrame = CGRectMake(0, -_cachedBaselineOffset,
213
+ self.bounds.size.width, self.bounds.size.height);
214
+ if (!CGRectEqualToRect(_drawingLayer.frame, newFrame)) {
215
+ [CATransaction begin];
216
+ [CATransaction setDisableActions:YES];
217
+ _drawingLayer.frame = newFrame;
218
+ [CATransaction commit];
219
+ }
220
+ // First layout after inline detection: the layer missed the initial
221
+ // setNeedsDisplay from updateProps (which targeted self before detection).
222
+ if (created) [_drawingLayer setNeedsDisplay];
160
223
  }
224
+ // standalone: draws directly via drawRect: on self
225
+ }
226
+
227
+ #pragma mark - Drawing
228
+
229
+ // Standalone icons draw directly in this view's drawRect:.
230
+ - (void)drawRect:(CGRect)rect {
231
+ if (_isInlineInText) return; // inline icons draw via _drawingLayer
232
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
233
+ if (ctx) [self _drawIconInContext:ctx bounds:self.bounds];
234
+ }
235
+
236
+ // Render multi-color icons by drawing each color layer glyph at the same
237
+ // position. Layers stack via painter's order to compose the final icon.
238
+ - (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds {
239
+ if (!_font || _glyphs.empty()) return;
240
+ if (!_metricsValid) [self _updateMetrics];
161
241
 
162
242
  CGContextSaveGState(context);
163
- // CoreText draws with y-axis pointing up; UIKit has y-axis pointing down.
164
- CGContextTranslateCTM(context, 0, self.bounds.size.height);
243
+ // Flip to CoreText coordinates (Y-up) and apply fit scale.
244
+ CGContextTranslateCTM(context, 0, bounds.size.height);
165
245
  CGContextScaleCTM(context, 1.0, -1.0);
166
246
  CGContextScaleCTM(context, _fitScale, _fitScale);
167
247
 
168
- // Batch consecutive same-color glyphs into a single CTFontDrawGlyphs call
169
248
  size_t i = 0;
170
249
  while (i < _glyphs.size()) {
171
- if (_glyphs[i] == 0) {
172
- i++;
173
- continue;
174
- }
250
+ if (_glyphs[i] == 0) { i++; continue; }
175
251
 
176
- // Determine the color for this run
177
252
  CGColorRef color = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
178
253
  if (!color) {
179
- // Fallback: opaque black
180
254
  static CGColorRef sBlack = CGColorCreateSRGB(0, 0, 0, 1);
181
255
  color = sBlack;
182
256
  }
183
257
  CGContextSetFillColorWithColor(context, color);
184
258
 
185
- // Collect consecutive glyphs with the same color
259
+ // Batch consecutive same-color glyphs.
186
260
  size_t batchStart = i;
187
261
  size_t batchCount = 0;
188
- // Use a small stack buffer for positions; heap-allocate only for very large batches
189
- CGPoint positionsBuf[16];
190
- CGGlyph glyphsBuf[16];
191
- CGPoint *positions = positionsBuf;
192
- CGGlyph *batchGlyphs = glyphsBuf;
262
+ CGPoint posBuf[16];
263
+ CGGlyph glyphBuf[16];
193
264
 
194
265
  while (i < _glyphs.size()) {
195
266
  if (_glyphs[i] == 0) { i++; continue; }
196
-
197
- CGColorRef nextColor = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
198
- // Break batch if color changes
199
- if (i > batchStart && nextColor != color) break;
200
-
267
+ CGColorRef next = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
268
+ if (i > batchStart && next != color) break;
201
269
  if (batchCount < 16) {
202
- positions[batchCount] = _baselinePosition;
203
- batchGlyphs[batchCount] = _glyphs[i];
270
+ posBuf[batchCount] = _baselinePosition;
271
+ glyphBuf[batchCount] = _glyphs[i];
204
272
  }
205
273
  batchCount++;
206
274
  i++;
207
275
  }
208
276
 
209
- // If batch exceeded stack buffer, allocate and refill
277
+ CGPoint *positions = posBuf;
278
+ CGGlyph *glyphs = glyphBuf;
210
279
  if (batchCount > 16) {
211
280
  positions = (CGPoint *)malloc(batchCount * sizeof(CGPoint));
212
- batchGlyphs = (CGGlyph *)malloc(batchCount * sizeof(CGGlyph));
281
+ glyphs = (CGGlyph *)malloc(batchCount * sizeof(CGGlyph));
213
282
  size_t idx = 0;
214
283
  for (size_t j = batchStart; j < i; j++) {
215
284
  if (_glyphs[j] == 0) continue;
216
285
  positions[idx] = _baselinePosition;
217
- batchGlyphs[idx] = _glyphs[j];
286
+ glyphs[idx] = _glyphs[j];
218
287
  idx++;
219
288
  }
220
289
  }
221
290
 
222
- CTFontDrawGlyphs(_font, batchGlyphs, positions, batchCount, context);
291
+ CTFontDrawGlyphs(_font, glyphs, positions, batchCount, context);
223
292
 
224
293
  if (batchCount > 16) {
225
294
  free(positions);
226
- free(batchGlyphs);
295
+ free(glyphs);
227
296
  }
228
297
  }
229
298
 
230
299
  CGContextRestoreGState(context);
231
300
  }
232
301
 
233
- - (void)dealloc
234
- {
235
- if (_font) {
236
- CFRelease(_font);
302
+ #pragma mark - Props
303
+
304
+ - (void)_releaseCachedColors {
305
+ for (CGColorRef c : _cachedCGColors) CGColorRelease(c);
306
+ _cachedCGColors.clear();
307
+ }
308
+
309
+ // Convert ARGB uint32 color values into cached CGColorRefs.
310
+ - (void)_rebuildCachedColors {
311
+ [self _releaseCachedColors];
312
+ _cachedCGColors.resize(_colors.size());
313
+ for (size_t i = 0; i < _colors.size(); i++) {
314
+ uint32_t ci = _colors[i];
315
+ _cachedCGColors[i] = CGColorCreateSRGB(
316
+ ((ci >> 16) & 0xFF) / 255.0,
317
+ ((ci >> 8) & 0xFF) / 255.0,
318
+ ( ci & 0xFF) / 255.0,
319
+ ((ci >> 24) & 0xFF) / 255.0);
320
+ }
321
+ }
322
+
323
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps {
324
+ const auto &oldViewProps = static_cast<const NanoIconViewProps &>(*_props);
325
+ const auto &newViewProps = static_cast<const NanoIconViewProps &>(*props);
326
+
327
+ BOOL fontChanged = NO;
328
+ BOOL needsRedraw = NO;
329
+
330
+ if (oldViewProps.fontFamily != newViewProps.fontFamily ||
331
+ oldViewProps.fontSize != newViewProps.fontSize) {
332
+ _fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
333
+ _fontSize = newViewProps.fontSize;
334
+ _font = NanoIconGetCachedFont(_fontFamily, _fontSize);
335
+ _metricsValid = NO;
336
+ fontChanged = YES;
337
+ needsRedraw = YES;
338
+ }
339
+
340
+ // Map Unicode codepoints to font glyph IDs, handling surrogate pairs for codepoints > 0xFFFF.
341
+ if (fontChanged || oldViewProps.codepoints != newViewProps.codepoints) {
342
+ const auto &codepoints = newViewProps.codepoints;
343
+ _glyphs.resize(codepoints.size());
344
+ for (size_t i = 0; i < codepoints.size(); i++) {
345
+ int32_t cp = codepoints[i];
346
+ if (cp <= 0xFFFF) {
347
+ UniChar ch = (UniChar)cp;
348
+ CTFontGetGlyphsForCharacters(_font, &ch, &_glyphs[i], 1);
349
+ } else {
350
+ UniChar surr[2] = {
351
+ (UniChar)(0xD800 + ((cp - 0x10000) >> 10)),
352
+ (UniChar)(0xDC00 + ((cp - 0x10000) & 0x3FF))
353
+ };
354
+ CGGlyph pair[2] = {0, 0};
355
+ CTFontGetGlyphsForCharacters(_font, surr, pair, 2);
356
+ _glyphs[i] = pair[0];
357
+ }
358
+ }
359
+ needsRedraw = YES;
237
360
  }
361
+
362
+ if (oldViewProps.colors != newViewProps.colors) {
363
+ const auto &colors = newViewProps.colors;
364
+ _colors.resize(colors.size());
365
+ for (size_t i = 0; i < colors.size(); i++) {
366
+ _colors[i] = (uint32_t)colors[i];
367
+ }
368
+ [self _rebuildCachedColors];
369
+ needsRedraw = YES;
370
+ }
371
+
372
+ [super updateProps:props oldProps:oldProps];
373
+ if (needsRedraw) {
374
+ if (_isInlineInText && _drawingLayer) {
375
+ [_drawingLayer setNeedsDisplay];
376
+ } else {
377
+ [self setNeedsDisplay];
378
+ }
379
+ }
380
+ }
381
+
382
+ - (void)dealloc {
383
+ // _font is borrowed from static cache — do not release
238
384
  [self _releaseCachedColors];
239
385
  }
240
386
 
241
387
  @end
242
388
 
243
- Class<RCTComponentViewProtocol> NanoIconViewCls(void)
244
- {
389
+ Class<RCTComponentViewProtocol> NanoIconViewCls(void) {
245
390
  return NanoIconView.class;
246
391
  }
File without changes
@@ -9,6 +9,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  export { shallowEqualColor };
10
10
  const DEFAULT_ICON_SIZE = 12;
11
11
  const HAS_NATIVE_IMPL = UIManager.hasViewManagerConfig('NanoIconView');
12
+
13
+ // Shared processColor cache — avoids redundant color parsing for repeated
14
+ // color strings like "black", "rgba(0,0,0,0.3)" across thousands of icons
15
+ const processedColorCache = new Map();
16
+ function cachedProcessColor(color) {
17
+ let result = processedColorCache.get(color);
18
+ if (result === undefined) {
19
+ result = processColor(color) ?? 0xff000000;
20
+ processedColorCache.set(color, result);
21
+ }
22
+ return result;
23
+ }
12
24
  export function createIconSet(glyphMap) {
13
25
  if (!HAS_NATIVE_IMPL) {
14
26
  return createJSIconSet(glyphMap);
@@ -18,6 +30,27 @@ export function createIconSet(glyphMap) {
18
30
  const resolveEntry = name => {
19
31
  return glyphMap.i[name] ?? [unitsPerEm, [[63, 'black']]];
20
32
  };
33
+
34
+ // Pre-compute per-icon static data (codepoints, default colors) once at set creation
35
+ // Avoids layers.map() + processColor per icon mount
36
+ const codepointsCache = new Map();
37
+ const defaultColorsCache = new Map();
38
+ function getCodepoints(name, layers) {
39
+ let cp = codepointsCache.get(name);
40
+ if (!cp) {
41
+ cp = layers.map(([c]) => c);
42
+ codepointsCache.set(name, cp);
43
+ }
44
+ return cp;
45
+ }
46
+ function getDefaultColors(name, layers) {
47
+ let colors = defaultColorsCache.get(name);
48
+ if (!colors) {
49
+ colors = layers.map(([, srcColor]) => cachedProcessColor(srcColor ?? 'black'));
50
+ defaultColorsCache.set(name, colors);
51
+ }
52
+ return colors;
53
+ }
21
54
  const Icon = /*#__PURE__*/memo(({
22
55
  name,
23
56
  size = DEFAULT_ICON_SIZE,
@@ -34,13 +67,20 @@ export function createIconSet(glyphMap) {
34
67
  const [adv, layers] = resolveEntry(name);
35
68
  const scaledSize = size * fontScale;
36
69
  const width = adv / unitsPerEm * scaledSize;
37
- const colorArray = Array.isArray(color) ? color : [color];
38
- const lastPaletteColor = colorArray?.length ? colorArray[colorArray.length - 1] : undefined;
39
- const codepoints = useMemo(() => layers.map(([cp]) => cp), [name]);
40
- const processedColors = useMemo(() => layers.map(([, srcColor], i) => {
41
- const layerColor = colorArray?.[i] ?? lastPaletteColor ?? srcColor ?? 'black';
42
- return processColor(layerColor) ?? 0xff000000;
43
- }), [name, color]);
70
+ const nameStr = name;
71
+ const codepoints = getCodepoints(nameStr, layers);
72
+ const processedColors = useMemo(() => {
73
+ // Fast path: no custom color use pre-computed defaults
74
+ if (color === undefined || color === null) {
75
+ return getDefaultColors(nameStr, layers);
76
+ }
77
+ const colorArray = Array.isArray(color) ? color : [color];
78
+ const lastPaletteColor = colorArray.length ? colorArray[colorArray.length - 1] : undefined;
79
+ return layers.map(([, srcColor], i) => {
80
+ const layerColor = colorArray[i] ?? lastPaletteColor ?? srcColor ?? 'black';
81
+ return cachedProcessColor(layerColor);
82
+ });
83
+ }, [nameStr, color]);
44
84
  const nativeStyle = useMemo(() => [{
45
85
  width,
46
86
  height: scaledSize
@@ -1 +1 @@
1
- {"version":3,"names":["memo","useMemo","PixelRatio","UIManager","processColor","shallowEqualColor","NanoIconViewNative","createJSIconSet","jsx","_jsx","DEFAULT_ICON_SIZE","HAS_NATIVE_IMPL","hasViewManagerConfig","createIconSet","glyphMap","fontFamilyBasename","m","f","unitsPerEm","u","resolveEntry","name","i","Icon","size","color","style","allowFontScaling","accessible","accessibilityLabel","accessibilityRole","testID","ref","fontScale","getFontScale","adv","layers","scaledSize","width","colorArray","Array","isArray","lastPaletteColor","length","undefined","codepoints","map","cp","processedColors","srcColor","layerColor","nativeStyle","height","fontFamily","colors","fontSize","advanceWidth","iconWidth","iconHeight","prev","next","displayName"],"sourceRoot":"../../src","sources":["createNanoIconsSet.native.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["memo","useMemo","PixelRatio","UIManager","processColor","shallowEqualColor","NanoIconViewNative","createJSIconSet","jsx","_jsx","DEFAULT_ICON_SIZE","HAS_NATIVE_IMPL","hasViewManagerConfig","processedColorCache","Map","cachedProcessColor","color","result","get","undefined","set","createIconSet","glyphMap","fontFamilyBasename","m","f","unitsPerEm","u","resolveEntry","name","i","codepointsCache","defaultColorsCache","getCodepoints","layers","cp","map","c","getDefaultColors","colors","srcColor","Icon","size","style","allowFontScaling","accessible","accessibilityLabel","accessibilityRole","testID","ref","fontScale","getFontScale","adv","scaledSize","width","nameStr","codepoints","processedColors","colorArray","Array","isArray","lastPaletteColor","length","layerColor","nativeStyle","height","fontFamily","fontSize","advanceWidth","iconWidth","iconHeight","prev","next","displayName"],"sourceRoot":"../../src","sources":["createNanoIconsSet.native.tsx"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"createNanoIconsSet.native.d.ts","sourceRoot":"","sources":["../../../src/createNanoIconsSet.native.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAc,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAI9D,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAM7B,wBAAgB,aAAa,CAAC,EAAE,SAAS,iBAAiB,EACxD,QAAQ,EAAE,EAAE,GACX,aAAa,CAAC,EAAE,CAAC,CA0FnB"}
1
+ {"version":3,"file":"createNanoIconsSet.native.d.ts","sourceRoot":"","sources":["../../../src/createNanoIconsSet.native.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAc,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAI9D,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAkB7B,wBAAgB,aAAa,CAAC,EAAE,SAAS,iBAAiB,EACxD,QAAQ,EAAE,EAAE,GACX,aAAa,CAAC,EAAE,CAAC,CA8GnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nano-icons",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Use any svg as font. High-performance, build-time icon font generation and rendering for React Native & Expo.",
5
5
  "react-native": "src/index.ts",
6
6
  "source": "src/index.ts",
@@ -13,6 +13,18 @@ const DEFAULT_ICON_SIZE = 12;
13
13
 
14
14
  const HAS_NATIVE_IMPL = UIManager.hasViewManagerConfig('NanoIconView');
15
15
 
16
+ // Shared processColor cache — avoids redundant color parsing for repeated
17
+ // color strings like "black", "rgba(0,0,0,0.3)" across thousands of icons
18
+ const processedColorCache = new Map<string, number>();
19
+ function cachedProcessColor(color: string): number {
20
+ let result = processedColorCache.get(color);
21
+ if (result === undefined) {
22
+ result = (processColor(color) ?? 0xff000000) as number;
23
+ processedColorCache.set(color, result);
24
+ }
25
+ return result;
26
+ }
27
+
16
28
  export function createIconSet<GM extends NanoGlyphMapInput>(
17
29
  glyphMap: GM
18
30
  ): IconComponent<GM> {
@@ -30,6 +42,29 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
30
42
  ]) as GlyphEntry;
31
43
  };
32
44
 
45
+ // Pre-compute per-icon static data (codepoints, default colors) once at set creation
46
+ // Avoids layers.map() + processColor per icon mount
47
+ const codepointsCache = new Map<string, readonly number[]>();
48
+ const defaultColorsCache = new Map<string, readonly number[]>();
49
+
50
+ function getCodepoints(name: string, layers: GlyphEntry[1]): readonly number[] {
51
+ let cp = codepointsCache.get(name);
52
+ if (!cp) {
53
+ cp = layers.map(([c]) => c);
54
+ codepointsCache.set(name, cp);
55
+ }
56
+ return cp;
57
+ }
58
+
59
+ function getDefaultColors(name: string, layers: GlyphEntry[1]): readonly number[] {
60
+ let colors = defaultColorsCache.get(name);
61
+ if (!colors) {
62
+ colors = layers.map(([, srcColor]) => cachedProcessColor(srcColor ?? 'black'));
63
+ defaultColorsCache.set(name, colors);
64
+ }
65
+ return colors;
66
+ }
67
+
33
68
  const Icon = memo(
34
69
  ({
35
70
  name,
@@ -48,27 +83,24 @@ export function createIconSet<GM extends NanoGlyphMapInput>(
48
83
  const scaledSize = size * fontScale;
49
84
  const width = (adv / unitsPerEm) * scaledSize;
50
85
 
51
- const colorArray = Array.isArray(color) ? color : [color];
52
- const lastPaletteColor = colorArray?.length
53
- ? colorArray[colorArray.length - 1]
54
- : undefined;
55
-
56
- const codepoints = useMemo(
57
- () => layers.map(([cp]) => cp),
58
-
59
- [name]
60
- );
61
-
62
- const processedColors = useMemo(
63
- () =>
64
- layers.map(([, srcColor], i) => {
65
- const layerColor =
66
- colorArray?.[i] ?? lastPaletteColor ?? srcColor ?? 'black';
67
- return (processColor(layerColor) ?? 0xff000000) as number;
68
- }),
69
-
70
- [name, color]
71
- );
86
+ const nameStr = name as string;
87
+ const codepoints = getCodepoints(nameStr, layers);
88
+
89
+ const processedColors = useMemo(() => {
90
+ // Fast path: no custom color — use pre-computed defaults
91
+ if (color === undefined || color === null) {
92
+ return getDefaultColors(nameStr, layers);
93
+ }
94
+ const colorArray = Array.isArray(color) ? color : [color];
95
+ const lastPaletteColor = colorArray.length
96
+ ? colorArray[colorArray.length - 1]
97
+ : undefined;
98
+ return layers.map(([, srcColor], i) => {
99
+ const layerColor =
100
+ colorArray[i] ?? lastPaletteColor ?? srcColor ?? 'black';
101
+ return cachedProcessColor(layerColor as string);
102
+ });
103
+ }, [nameStr, color]);
72
104
 
73
105
  const nativeStyle = useMemo(
74
106
  () => [{ width, height: scaledSize }, style],