react-native-nano-icons 0.1.4 → 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.
- package/ios/NanoIconView.mm +148 -43
- package/package.json +1 -1
package/ios/NanoIconView.mm
CHANGED
|
@@ -7,30 +7,41 @@
|
|
|
7
7
|
|
|
8
8
|
using namespace facebook::react;
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
@interface
|
|
12
|
-
|
|
10
|
+
// Forward-declare so the layer subclass can call the drawing method.
|
|
11
|
+
@interface NanoIconView ()
|
|
12
|
+
- (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds;
|
|
13
13
|
@end
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- (void)drawRect:(CGRect)rect {
|
|
25
|
-
if (self.drawBlock) {
|
|
26
|
-
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
27
|
-
if (ctx) self.drawBlock(ctx, self.bounds);
|
|
28
|
-
}
|
|
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];
|
|
29
24
|
}
|
|
30
25
|
@end
|
|
31
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
|
+
|
|
32
43
|
@implementation NanoIconView {
|
|
33
|
-
CTFontRef _font;
|
|
44
|
+
CTFontRef _font; // borrowed from static cache — do NOT CFRelease
|
|
34
45
|
NSString *_fontFamily;
|
|
35
46
|
CGFloat _fontSize;
|
|
36
47
|
std::vector<CGGlyph> _glyphs;
|
|
@@ -39,7 +50,19 @@ using namespace facebook::react;
|
|
|
39
50
|
CGFloat _fitScale;
|
|
40
51
|
CGPoint _baselinePosition;
|
|
41
52
|
BOOL _metricsValid;
|
|
42
|
-
|
|
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;
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
@@ -49,16 +72,12 @@ using namespace facebook::react;
|
|
|
49
72
|
self.opaque = NO;
|
|
50
73
|
self.backgroundColor = [UIColor clearColor];
|
|
51
74
|
self.clipsToBounds = NO;
|
|
75
|
+
self.contentMode = UIViewContentModeRedraw;
|
|
52
76
|
|
|
53
77
|
_fitScale = 1.0;
|
|
54
78
|
_baselinePosition = CGPointZero;
|
|
55
79
|
|
|
56
|
-
|
|
57
|
-
__weak __typeof(self) weakSelf = self;
|
|
58
|
-
_drawingView.drawBlock = ^(CGContextRef context, CGRect bounds) {
|
|
59
|
-
[weakSelf _drawIconInContext:context bounds:bounds];
|
|
60
|
-
};
|
|
61
|
-
[self addSubview:_drawingView];
|
|
80
|
+
// _drawingLayer is created lazily only when inline in Text
|
|
62
81
|
}
|
|
63
82
|
return self;
|
|
64
83
|
}
|
|
@@ -87,23 +106,47 @@ using namespace facebook::react;
|
|
|
87
106
|
_metricsValid = YES;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
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.
|
|
93
123
|
UIView *current = self.superview;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if ([current
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
99
129
|
}
|
|
100
130
|
current = current.superview;
|
|
101
131
|
}
|
|
102
|
-
|
|
132
|
+
|
|
133
|
+
if (!target) return;
|
|
134
|
+
|
|
135
|
+
NSAttributedString *attrStr = nil;
|
|
136
|
+
if ([target respondsToSelector:@selector(attributedText)]) {
|
|
137
|
+
attrStr = [target performSelector:@selector(attributedText)];
|
|
138
|
+
}
|
|
139
|
+
if (!attrStr || attrStr.length == 0) return;
|
|
103
140
|
|
|
104
141
|
UIFont *f = [attrStr attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
|
|
105
|
-
if (!f) return
|
|
142
|
+
if (!f) return;
|
|
143
|
+
|
|
144
|
+
_isInlineInText = YES;
|
|
145
|
+
_paragraphView = target;
|
|
106
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.
|
|
107
150
|
NSParagraphStyle *style = [attrStr attribute:NSParagraphStyleAttributeName
|
|
108
151
|
atIndex:0 effectiveRange:nil];
|
|
109
152
|
CGFloat lineHeight = (style && style.maximumLineHeight > 0)
|
|
@@ -117,22 +160,79 @@ using namespace facebook::react;
|
|
|
117
160
|
CGFloat posInLine = fmod(frameBottom, lineHeight);
|
|
118
161
|
if (posInLine < 0.01) posInLine = lineHeight;
|
|
119
162
|
|
|
120
|
-
|
|
163
|
+
_cachedBaselineOffset = MAX(0, posInLine - baselineFromLineTop);
|
|
164
|
+
}
|
|
165
|
+
|
|
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;
|
|
176
|
+
}
|
|
177
|
+
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
[super setBounds:bounds];
|
|
121
191
|
}
|
|
122
192
|
|
|
123
193
|
#pragma mark - Layout
|
|
124
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.
|
|
125
198
|
- (void)layoutSubviews {
|
|
126
199
|
[super layoutSubviews];
|
|
127
200
|
if (!_metricsValid) [self _updateMetrics];
|
|
128
201
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
202
|
+
if (!_inlineDetected) {
|
|
203
|
+
[self _detectAndCacheInlineState];
|
|
204
|
+
}
|
|
205
|
+
|
|
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];
|
|
223
|
+
}
|
|
224
|
+
// standalone: draws directly via drawRect: on self
|
|
132
225
|
}
|
|
133
226
|
|
|
134
227
|
#pragma mark - Drawing
|
|
135
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
|
+
|
|
136
236
|
// Render multi-color icons by drawing each color layer glyph at the same
|
|
137
237
|
// position. Layers stack via painter's order to compose the final icon.
|
|
138
238
|
- (void)_drawIconInContext:(CGContextRef)context bounds:(CGRect)bounds {
|
|
@@ -229,10 +329,9 @@ using namespace facebook::react;
|
|
|
229
329
|
|
|
230
330
|
if (oldViewProps.fontFamily != newViewProps.fontFamily ||
|
|
231
331
|
oldViewProps.fontSize != newViewProps.fontSize) {
|
|
232
|
-
if (_font) { CFRelease(_font); _font = NULL; }
|
|
233
332
|
_fontFamily = [NSString stringWithUTF8String:newViewProps.fontFamily.c_str()];
|
|
234
333
|
_fontSize = newViewProps.fontSize;
|
|
235
|
-
_font =
|
|
334
|
+
_font = NanoIconGetCachedFont(_fontFamily, _fontSize);
|
|
236
335
|
_metricsValid = NO;
|
|
237
336
|
fontChanged = YES;
|
|
238
337
|
needsRedraw = YES;
|
|
@@ -271,11 +370,17 @@ using namespace facebook::react;
|
|
|
271
370
|
}
|
|
272
371
|
|
|
273
372
|
[super updateProps:props oldProps:oldProps];
|
|
274
|
-
if (needsRedraw)
|
|
373
|
+
if (needsRedraw) {
|
|
374
|
+
if (_isInlineInText && _drawingLayer) {
|
|
375
|
+
[_drawingLayer setNeedsDisplay];
|
|
376
|
+
} else {
|
|
377
|
+
[self setNeedsDisplay];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
275
380
|
}
|
|
276
381
|
|
|
277
382
|
- (void)dealloc {
|
|
278
|
-
|
|
383
|
+
// _font is borrowed from static cache — do not release
|
|
279
384
|
[self _releaseCachedColors];
|
|
280
385
|
}
|
|
281
386
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nano-icons",
|
|
3
|
-
"version": "0.1.
|
|
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",
|