react-native-nano-icons 0.1.2 → 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/README.md +20 -164
- package/android/build.gradle +28 -0
- package/android/src/main/java/com/nanoicons/NanoIconView.kt +78 -0
- package/android/src/main/java/com/nanoicons/NanoIconViewManager.kt +84 -0
- package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +22 -0
- package/ios/NanoIconView.h +4 -0
- package/ios/NanoIconView.mm +246 -0
- package/lib/commonjs/cli/build.js +1 -1
- package/lib/commonjs/cli/config.d.ts +2 -2
- package/lib/commonjs/cli/config.js +7 -6
- package/lib/commonjs/scripts/cli.js +15 -5
- package/lib/commonjs/src/core/font/compile.d.ts +13 -2
- package/lib/commonjs/src/core/font/compile.js +49 -46
- package/lib/commonjs/src/core/pipeline/managers.js +19 -3
- package/lib/commonjs/src/core/pipeline/run.js +121 -32
- package/lib/commonjs/src/core/svg/layers.d.ts +16 -0
- package/lib/commonjs/src/core/svg/layers.js +27 -0
- package/lib/commonjs/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/commonjs/src/core/svg/svg_dom.js +78 -2
- package/lib/commonjs/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/commonjs/src/core/svg/svg_pathops.js +209 -19
- package/lib/commonjs/src/core/types.d.ts +30 -15
- package/lib/module/core/font/compile.js +52 -41
- package/lib/module/core/font/compile.js.map +1 -1
- package/lib/module/core/pipeline/managers.js +17 -3
- package/lib/module/core/pipeline/managers.js.map +1 -1
- package/lib/module/core/pipeline/run.js +131 -44
- package/lib/module/core/pipeline/run.js.map +1 -1
- package/lib/module/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/lib/module/core/svg/layers.js +34 -0
- package/lib/module/core/svg/layers.js.map +1 -1
- package/lib/module/core/svg/svg_dom.js +91 -4
- package/lib/module/core/svg/svg_dom.js.map +1 -1
- package/lib/module/core/svg/svg_pathops.js +203 -19
- package/lib/module/core/svg/svg_pathops.js.map +1 -1
- package/lib/module/createNanoIconsSet.js +3 -79
- package/lib/module/createNanoIconsSet.js.map +1 -1
- package/lib/module/createNanoIconsSet.native.js +68 -0
- package/lib/module/createNanoIconsSet.native.js.map +1 -0
- package/lib/module/createNanoIconsSet.shared.js +91 -0
- package/lib/module/createNanoIconsSet.shared.js.map +1 -0
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/NanoIconViewNativeComponent.ts +15 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/shallowEqualColor.js +15 -0
- package/lib/module/utils/shallowEqualColor.js.map +1 -0
- package/lib/typescript/src/core/font/compile.d.ts +13 -2
- package/lib/typescript/src/core/font/compile.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/layers.d.ts +16 -0
- package/lib/typescript/src/core/svg/layers.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts +29 -1
- package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
- package/lib/typescript/src/core/svg/svg_pathops.d.ts +11 -0
- package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +30 -15
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.d.ts +5 -18
- package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
- package/lib/typescript/src/createNanoIconsSet.native.d.ts +7 -0
- package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
- package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +14 -0
- package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +19 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts +4 -0
- package/lib/typescript/src/utils/shallowEqualColor.d.ts.map +1 -0
- package/package.json +22 -5
- package/react-native-nano-icons.podspec +18 -0
- package/scripts/cli.ts +14 -5
- package/src/core/font/compile.ts +65 -61
- package/src/core/pipeline/managers.ts +26 -3
- package/src/core/pipeline/run.ts +156 -38
- package/src/core/shims/picosvg-0.22.3-py3-none-any.whl +0 -0
- package/src/core/svg/layers.ts +44 -0
- package/src/core/svg/svg_dom.ts +96 -4
- package/src/core/svg/svg_pathops.ts +245 -27
- package/src/core/types.ts +21 -10
- package/src/createNanoIconsSet.native.tsx +108 -0
- package/src/createNanoIconsSet.shared.tsx +121 -0
- package/src/createNanoIconsSet.tsx +7 -126
- package/src/index.ts +1 -2
- package/src/specs/NanoIconViewNativeComponent.ts +15 -0
- package/src/types.ts +27 -0
- package/src/utils/shallowEqualColor.ts +17 -0
package/README.md
CHANGED
|
@@ -1,174 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
4
5
|
|
|
5
|
-
`react-native-nano-icons` automates the conversion of SVG directories into optimized, **multi-color-aware** native fonts and strictly typed TypeScript component factories. It leverages a WebAssembly-powered skia pathops binary build pipeline to ensure pixel-perfect geometry and zero runtime overhead.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<br>
|
|
8
|
+
</div>
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
# High-performance, build-time icon font generation for React Native & Expo.
|
|
10
11
|
|
|
11
|
-
-
|
|
12
|
-
- [x] Android
|
|
13
|
-
- [x] Web
|
|
12
|
+
Start here ➡️➡️➡️ [react-native-nano-icons README.md](/README.md) ⬅️⬅️⬅️
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
> `<filter>` and `<mask>` are not yet supported, due to native fonts' glyph limitations.
|
|
17
|
-
> In order to leverage those features, use [`react-native-svg`](https://github.com/software-mansion/react-native-svg) or [`expo-image`](https://docs.expo.dev/versions/latest/sdk/image/)
|
|
14
|
+
## Nano Icons are created by Software Mansion
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
[](https://swmansion.com)
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
Since 2012 [Software Mansion](https://swmansion.com) is a software agency with
|
|
19
|
+
experience in building web and mobile apps. We are Core React Native
|
|
20
|
+
Contributors and experts in dealing with all kinds of React Native issues. We
|
|
21
|
+
can help you build your next dream product –
|
|
22
|
+
[Hire us](https://swmansion.com/contact/projects?utm_source=typegpu&utm_medium=readme).
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
npm install react-native-nano-icons
|
|
25
|
-
```
|
|
24
|
+
<!-- automd:contributors author="software-mansion" -->
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`app.json`
|
|
34
|
-
|
|
35
|
-
```JSON
|
|
36
|
-
{
|
|
37
|
-
"expo": {
|
|
38
|
-
"plugins": [
|
|
39
|
-
[
|
|
40
|
-
"react-native-nano-icons",
|
|
41
|
-
{
|
|
42
|
-
"iconSets": [
|
|
43
|
-
{
|
|
44
|
-
"inputDir": "./assets/icons/user"
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
<details>
|
|
55
|
-
<summary>All iconSets Entry Plugin Options</summary>
|
|
56
|
-
|
|
57
|
-
The plugin accepts an object with an `iconSets` array, allowing you to generate multiple distinct fonts in a single build.
|
|
58
|
-
|
|
59
|
-
| Property | Type | Required | Default | Description |
|
|
60
|
-
| :------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
-
| `inputDir` | `string` | **Yes** | — | Path to the directory containing your `.svg` files (e.g., `./assets/icons/ui`). |
|
|
62
|
-
| `fontFamily` | `string` | No | Folder Name | The name of the generated font family and file. If omitted, the name of the `inputDir` folder is used (e.g., `ui`). |
|
|
63
|
-
| `outputDir` | `string` | No | `../nanoicons` | Path where the `.ttf` and `.json` artifacts will be saved. Defaults to a sibling `nanoicons` folder relative to the input. |
|
|
64
|
-
| `upm` | `number` | No | `1024` | Units Per Em. Defines the resolution of the font grid. |
|
|
65
|
-
| `startUnicode` | `string` | No | `0xe900` | The starting Hex Unicode point for the first icon glyph. |
|
|
66
|
-
|
|
67
|
-
<details>
|
|
68
|
-
<summary>Default Dir Path Behavior</summary>
|
|
69
|
-
If you do not specify an `outputDir` or `fontFamily`, the library attempts to keep your project organized by creating a sibling folder.
|
|
70
|
-
|
|
71
|
-
- **Input:** `./assets/icons/user`
|
|
72
|
-
- **Resulting Output:** `./assets/icons/nanoicons/user.ttf` & `user.glyphmap.json`
|
|
73
|
-
</details>
|
|
74
|
-
</details>
|
|
75
|
-
|
|
76
|
-
#### 2.2 Bare React Native/React Native Web (no Expo)
|
|
77
|
-
|
|
78
|
-
Bare apps don’t have a prebuild step, so you run the same pipeline via the CLI and ship the built fonts into the native project yourself:
|
|
79
|
-
|
|
80
|
-
1. **Config** – At the app root, add a `.nanoicons.json` with the same `iconSets` shape as the Expo plugin (see "All iconSets Entry Plugin Options above").
|
|
81
|
-
<details>
|
|
82
|
-
<summary>.nanoicons.json example</summary>
|
|
83
|
-
|
|
84
|
-
```JSON
|
|
85
|
-
{
|
|
86
|
-
"iconSets": [
|
|
87
|
-
{
|
|
88
|
-
"inputDir": "./assets/icons/user"
|
|
89
|
-
}
|
|
90
|
-
]
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
</details>
|
|
95
|
-
|
|
96
|
-
2. **Build and link** – From the app root run:
|
|
97
|
-
|
|
98
|
-
```sh
|
|
99
|
-
npx react-native-nano-icons
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
This works exactly like the config plugin, removing any necessity for manual Xcode/Android Studio steps.
|
|
103
|
-
|
|
104
|
-
### 3. Creating an Icon Set Component
|
|
105
|
-
|
|
106
|
-
Use the factory function to create a fully typed component for your specific icon set. This enables multiple distinct sets (e.g., "Outlined", "Solid", "Brand") within a single app.
|
|
107
|
-
|
|
108
|
-
`src/components/UserIcon.tsx`
|
|
109
|
-
|
|
110
|
-
```TypeScript
|
|
111
|
-
import { createNanoIconSet } from "react-native-nano-icons";
|
|
112
|
-
// auto-generated during build-time in outputDir
|
|
113
|
-
import glyphMap from "../../assets/nanoicons/UserIcons.glyphmap.json";
|
|
114
|
-
|
|
115
|
-
export const UserIcon = createNanoIconSet(glyphMap);
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### 4. Component Usage
|
|
119
|
-
|
|
120
|
-
The generated component supports standard `Text` props **excluding** `style.color | .fontWeight | .fontFamily`.
|
|
121
|
-
|
|
122
|
-
To manipulate the color(s) of the icon you should provide `colorPalette: ColorValue[]`.
|
|
123
|
-
|
|
124
|
-
The `name` prop corresponds to **the original name of the svg file** for a given icon.
|
|
125
|
-
|
|
126
|
-
```TypeScript
|
|
127
|
-
import { Text } from 'react-native'
|
|
128
|
-
import { UserIcon } from './components/UserIcon';
|
|
129
|
-
|
|
130
|
-
export default function App() {
|
|
131
|
-
return (
|
|
132
|
-
<>
|
|
133
|
-
// Renders the icon with its original multi-color layers from the SVG
|
|
134
|
-
<UserIcon name="avatar-1" size={32} />
|
|
135
|
-
|
|
136
|
-
// Overrides all color layers with the provided colors respectively
|
|
137
|
-
<UserIcon name="avatar-1" size={24} colorPalette={["blue", "#ffffff", "#fc2930"]} />
|
|
138
|
-
|
|
139
|
-
// Renders icon inline within a paragraph
|
|
140
|
-
<Text>
|
|
141
|
-
User <UserIcon name="avatar-1" size={12} /> liked your photo!
|
|
142
|
-
</Text>
|
|
143
|
-
</>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Your color icons can have as many colors as your original svg has, therefore you should experiment to establish which element of the array corresponds to the layer you aim to change the color of.
|
|
149
|
-
If the icon is single-color by design (which results in creating a single glyph during build-time) only the first element is took into consideration, and if the `colorPalette` array is too short - the last color is repeated.
|
|
150
|
-
|
|
151
|
-
### 5. Font Regeneration
|
|
152
|
-
|
|
153
|
-
The script detects changes in path and contents of the SVGs in your input directory based on a fingerprint hash. If anything changed, or the output font files are deleted, a given icon-set is regenerated.
|
|
154
|
-
|
|
155
|
-
> [!IMPORTANT]
|
|
156
|
-
> **You should always verify your icons visually.**
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## 💡 Architecture & Pipeline & Examples
|
|
161
|
-
|
|
162
|
-
see [MOTIVATION.md](docs/MOTIVATION.md)
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## License
|
|
167
|
-
|
|
168
|
-
`react-native-nano-icons` is released under the **MIT License**. See [LICENSE](LICENSE) for the full text.
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
Built by [Software Mansion](https://swmansion.com/).
|
|
173
|
-
|
|
174
|
-
[<img width="128" height="69" alt="Software Mansion Logo" src="https://github.com/user-attachments/assets/f0e18471-a7aa-4e80-86ac-87686a86fe56" />](https://swmansion.com/)
|
|
26
|
+
Made by [@software-mansion](https://github.com/software-mansion) 💛
|
|
27
|
+
<br><br>
|
|
28
|
+
<a href="https://github.com/software-mansion-labs/react-native-nano-icons/graphs/contributors">
|
|
29
|
+
<img src="https://contrib.rocks/image?repo=software-mansion-labs/react-native-nano-icons" />
|
|
30
|
+
</a>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
apply plugin: 'com.android.library'
|
|
8
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
9
|
+
apply plugin: 'com.facebook.react'
|
|
10
|
+
|
|
11
|
+
android {
|
|
12
|
+
namespace "com.nanoicons"
|
|
13
|
+
compileSdk safeExtGet('compileSdkVersion', 35)
|
|
14
|
+
|
|
15
|
+
defaultConfig {
|
|
16
|
+
minSdk safeExtGet('minSdkVersion', 24)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
sourceSets {
|
|
20
|
+
main {
|
|
21
|
+
java.srcDirs += ["src/main/java"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dependencies {
|
|
27
|
+
implementation 'com.facebook.react:react-android'
|
|
28
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package com.nanoicons
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Paint
|
|
6
|
+
import android.graphics.Typeface
|
|
7
|
+
import android.view.View
|
|
8
|
+
import com.facebook.react.common.assets.ReactFontManager
|
|
9
|
+
|
|
10
|
+
class NanoIconView(context: Context) : View(context) {
|
|
11
|
+
|
|
12
|
+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
13
|
+
private var codepoints: IntArray = intArrayOf()
|
|
14
|
+
private var colors: IntArray = intArrayOf()
|
|
15
|
+
private var cachedFontFamily: String? = null
|
|
16
|
+
private var cachedTypeface: Typeface? = null
|
|
17
|
+
// Cached String objects — rebuilt only when codepoints change
|
|
18
|
+
private var cachedTexts: Array<String> = emptyArray()
|
|
19
|
+
// Cached baseline — rebuilt only when font or size changes
|
|
20
|
+
private var cachedBaseline: Float = 0f
|
|
21
|
+
|
|
22
|
+
init {
|
|
23
|
+
// Transparent background, no default drawing
|
|
24
|
+
setBackgroundColor(0x00000000)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun setFontFamily(fontFamily: String) {
|
|
28
|
+
if (fontFamily != cachedFontFamily) {
|
|
29
|
+
cachedFontFamily = fontFamily
|
|
30
|
+
cachedTypeface = ReactFontManager.getInstance()
|
|
31
|
+
.getTypeface(fontFamily, Typeface.NORMAL, context.assets)
|
|
32
|
+
paint.typeface = cachedTypeface
|
|
33
|
+
updateBaseline()
|
|
34
|
+
invalidate()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun setFontSize(size: Float) {
|
|
39
|
+
val sizeInPx = size * resources.displayMetrics.density
|
|
40
|
+
if (paint.textSize != sizeInPx) {
|
|
41
|
+
paint.textSize = sizeInPx
|
|
42
|
+
updateBaseline()
|
|
43
|
+
invalidate()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun setCodepoints(values: IntArray) {
|
|
48
|
+
codepoints = values
|
|
49
|
+
cachedTexts = Array(values.size) { i -> String(Character.toChars(values[i])) }
|
|
50
|
+
invalidate()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun setColors(values: IntArray) {
|
|
54
|
+
colors = values
|
|
55
|
+
invalidate()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun updateBaseline() {
|
|
59
|
+
cachedBaseline = -paint.fontMetrics.ascent
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun onDraw(canvas: Canvas) {
|
|
63
|
+
super.onDraw(canvas)
|
|
64
|
+
if (cachedTexts.isEmpty() || cachedTypeface == null) return
|
|
65
|
+
|
|
66
|
+
canvas.save()
|
|
67
|
+
canvas.clipRect(0f, 0f, width.toFloat(), height.toFloat())
|
|
68
|
+
|
|
69
|
+
// All layers drawn at the same position (stacked on each other)
|
|
70
|
+
for (i in cachedTexts.indices) {
|
|
71
|
+
val color = if (i < colors.size) colors[i] else 0xFF000000.toInt()
|
|
72
|
+
paint.color = color
|
|
73
|
+
canvas.drawText(cachedTexts[i], 0f, cachedBaseline, paint)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
canvas.restore()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package com.nanoicons
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
8
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
9
|
+
import com.facebook.react.viewmanagers.NanoIconViewManagerDelegate
|
|
10
|
+
import com.facebook.react.viewmanagers.NanoIconViewManagerInterface
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = NanoIconViewManager.REACT_CLASS)
|
|
13
|
+
class NanoIconViewManager :
|
|
14
|
+
SimpleViewManager<NanoIconView>(),
|
|
15
|
+
NanoIconViewManagerInterface<NanoIconView> {
|
|
16
|
+
|
|
17
|
+
private val delegate: ViewManagerDelegate<NanoIconView> =
|
|
18
|
+
NanoIconViewManagerDelegate(this)
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
const val REACT_CLASS = "NanoIconView"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun getName(): String = REACT_CLASS
|
|
25
|
+
|
|
26
|
+
override fun createViewInstance(reactContext: ThemedReactContext): NanoIconView =
|
|
27
|
+
NanoIconView(reactContext)
|
|
28
|
+
|
|
29
|
+
override fun getDelegate(): ViewManagerDelegate<NanoIconView> = delegate
|
|
30
|
+
|
|
31
|
+
@ReactProp(name = "fontFamily")
|
|
32
|
+
override fun setFontFamily(view: NanoIconView, value: String?) {
|
|
33
|
+
if (value != null) {
|
|
34
|
+
view.setFontFamily(value)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@ReactProp(name = "codepoints")
|
|
39
|
+
override fun setCodepoints(view: NanoIconView, value: ReadableArray?) {
|
|
40
|
+
if (value != null) {
|
|
41
|
+
val arr = IntArray(value.size())
|
|
42
|
+
for (i in 0 until value.size()) {
|
|
43
|
+
arr[i] = value.getInt(i)
|
|
44
|
+
}
|
|
45
|
+
view.setCodepoints(arr)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@ReactProp(name = "colors")
|
|
50
|
+
override fun setColors(view: NanoIconView, value: ReadableArray?) {
|
|
51
|
+
if (value != null) {
|
|
52
|
+
val arr = IntArray(value.size())
|
|
53
|
+
for (i in 0 until value.size()) {
|
|
54
|
+
arr[i] = value.getInt(i)
|
|
55
|
+
}
|
|
56
|
+
view.setColors(arr)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@ReactProp(name = "fontSize", defaultFloat = 12f)
|
|
61
|
+
override fun setFontSize(view: NanoIconView, value: Float) {
|
|
62
|
+
view.setFontSize(value)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@ReactProp(name = "advanceWidth", defaultInt = 0)
|
|
66
|
+
override fun setAdvanceWidth(view: NanoIconView, value: Int) {
|
|
67
|
+
// Used for sizing on JS side; native view uses Canvas layout
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@ReactProp(name = "unitsPerEm", defaultInt = 0)
|
|
71
|
+
override fun setUnitsPerEm(view: NanoIconView, value: Int) {
|
|
72
|
+
// Used for sizing on JS side; native view uses Canvas layout
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactProp(name = "iconWidth", defaultFloat = 0f)
|
|
76
|
+
override fun setIconWidth(view: NanoIconView, value: Float) {
|
|
77
|
+
// Width set via style from JS
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@ReactProp(name = "iconHeight", defaultFloat = 0f)
|
|
81
|
+
override fun setIconHeight(view: NanoIconView, value: Float) {
|
|
82
|
+
// Height set via style from JS
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.nanoicons
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
import com.facebook.react.uimanager.ViewManager
|
|
8
|
+
|
|
9
|
+
class NanoIconsPackage : BaseReactPackage() {
|
|
10
|
+
|
|
11
|
+
override fun createViewManagers(
|
|
12
|
+
reactContext: ReactApplicationContext
|
|
13
|
+
): List<ViewManager<*, *>> = listOf(NanoIconViewManager())
|
|
14
|
+
|
|
15
|
+
override fun getModule(
|
|
16
|
+
name: String,
|
|
17
|
+
reactContext: ReactApplicationContext
|
|
18
|
+
): NativeModule? = null
|
|
19
|
+
|
|
20
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
|
|
21
|
+
ReactModuleInfoProvider { emptyMap() }
|
|
22
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#import "NanoIconView.h"
|
|
2
|
+
|
|
3
|
+
#import <CoreText/CoreText.h>
|
|
4
|
+
#import <React/RCTConversions.h>
|
|
5
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
6
|
+
#import <react/renderer/components/RNNanoIconsSpec/ComponentDescriptors.h>
|
|
7
|
+
#import <react/renderer/components/RNNanoIconsSpec/Props.h>
|
|
8
|
+
|
|
9
|
+
using namespace facebook::react;
|
|
10
|
+
|
|
11
|
+
@implementation NanoIconView {
|
|
12
|
+
CTFontRef _font;
|
|
13
|
+
NSString *_fontFamily;
|
|
14
|
+
CGFloat _fontSize;
|
|
15
|
+
std::vector<CGGlyph> _glyphs;
|
|
16
|
+
std::vector<uint32_t> _colors;
|
|
17
|
+
// Cached CGColor refs — rebuilt only when colors prop changes
|
|
18
|
+
std::vector<CGColorRef> _cachedCGColors;
|
|
19
|
+
// Cached layout metrics — rebuilt only when font or bounds change
|
|
20
|
+
CGFloat _fitScale;
|
|
21
|
+
CGPoint _baselinePosition;
|
|
22
|
+
BOOL _metricsValid;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
26
|
+
{
|
|
27
|
+
if (self = [super initWithFrame:frame]) {
|
|
28
|
+
static const auto defaultProps = std::make_shared<const NanoIconViewProps>();
|
|
29
|
+
_props = defaultProps;
|
|
30
|
+
self.opaque = NO;
|
|
31
|
+
self.backgroundColor = [UIColor clearColor];
|
|
32
|
+
_fitScale = 1.0;
|
|
33
|
+
_baselinePosition = CGPointZero;
|
|
34
|
+
_metricsValid = NO;
|
|
35
|
+
}
|
|
36
|
+
return self;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
40
|
+
{
|
|
41
|
+
return concreteComponentDescriptorProvider<NanoIconViewComponentDescriptor>();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)_releaseCachedColors
|
|
45
|
+
{
|
|
46
|
+
for (CGColorRef c : _cachedCGColors) {
|
|
47
|
+
CGColorRelease(c);
|
|
48
|
+
}
|
|
49
|
+
_cachedCGColors.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
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
|
+
}
|
|
65
|
+
|
|
66
|
+
- (void)_updateMetrics
|
|
67
|
+
{
|
|
68
|
+
if (!_font) {
|
|
69
|
+
_metricsValid = NO;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
CGFloat ascent = CTFontGetAscent(_font);
|
|
74
|
+
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;
|
|
79
|
+
_baselinePosition = CGPointMake(0, descent);
|
|
80
|
+
_metricsValid = YES;
|
|
81
|
+
}
|
|
82
|
+
|
|
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;
|
|
94
|
+
}
|
|
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;
|
|
101
|
+
}
|
|
102
|
+
|
|
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());
|
|
108
|
+
|
|
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
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
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
|
+
}
|
|
136
|
+
|
|
137
|
+
[super updateProps:props oldProps:oldProps];
|
|
138
|
+
[self setNeedsDisplay];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
- (void)layoutSubviews
|
|
142
|
+
{
|
|
143
|
+
[super layoutSubviews];
|
|
144
|
+
_metricsValid = NO;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
- (void)drawRect:(CGRect)rect
|
|
148
|
+
{
|
|
149
|
+
if (!_font || _glyphs.empty()) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
154
|
+
if (!context) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!_metricsValid) {
|
|
159
|
+
[self _updateMetrics];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
CGContextSaveGState(context);
|
|
163
|
+
// CoreText draws with y-axis pointing up; UIKit has y-axis pointing down.
|
|
164
|
+
CGContextTranslateCTM(context, 0, self.bounds.size.height);
|
|
165
|
+
CGContextScaleCTM(context, 1.0, -1.0);
|
|
166
|
+
CGContextScaleCTM(context, _fitScale, _fitScale);
|
|
167
|
+
|
|
168
|
+
// Batch consecutive same-color glyphs into a single CTFontDrawGlyphs call
|
|
169
|
+
size_t i = 0;
|
|
170
|
+
while (i < _glyphs.size()) {
|
|
171
|
+
if (_glyphs[i] == 0) {
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Determine the color for this run
|
|
177
|
+
CGColorRef color = (i < _cachedCGColors.size()) ? _cachedCGColors[i] : NULL;
|
|
178
|
+
if (!color) {
|
|
179
|
+
// Fallback: opaque black
|
|
180
|
+
static CGColorRef sBlack = CGColorCreateSRGB(0, 0, 0, 1);
|
|
181
|
+
color = sBlack;
|
|
182
|
+
}
|
|
183
|
+
CGContextSetFillColorWithColor(context, color);
|
|
184
|
+
|
|
185
|
+
// Collect consecutive glyphs with the same color
|
|
186
|
+
size_t batchStart = i;
|
|
187
|
+
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;
|
|
193
|
+
|
|
194
|
+
while (i < _glyphs.size()) {
|
|
195
|
+
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
|
+
|
|
201
|
+
if (batchCount < 16) {
|
|
202
|
+
positions[batchCount] = _baselinePosition;
|
|
203
|
+
batchGlyphs[batchCount] = _glyphs[i];
|
|
204
|
+
}
|
|
205
|
+
batchCount++;
|
|
206
|
+
i++;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// If batch exceeded stack buffer, allocate and refill
|
|
210
|
+
if (batchCount > 16) {
|
|
211
|
+
positions = (CGPoint *)malloc(batchCount * sizeof(CGPoint));
|
|
212
|
+
batchGlyphs = (CGGlyph *)malloc(batchCount * sizeof(CGGlyph));
|
|
213
|
+
size_t idx = 0;
|
|
214
|
+
for (size_t j = batchStart; j < i; j++) {
|
|
215
|
+
if (_glyphs[j] == 0) continue;
|
|
216
|
+
positions[idx] = _baselinePosition;
|
|
217
|
+
batchGlyphs[idx] = _glyphs[j];
|
|
218
|
+
idx++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
CTFontDrawGlyphs(_font, batchGlyphs, positions, batchCount, context);
|
|
223
|
+
|
|
224
|
+
if (batchCount > 16) {
|
|
225
|
+
free(positions);
|
|
226
|
+
free(batchGlyphs);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
CGContextRestoreGState(context);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
- (void)dealloc
|
|
234
|
+
{
|
|
235
|
+
if (_font) {
|
|
236
|
+
CFRelease(_font);
|
|
237
|
+
}
|
|
238
|
+
[self _releaseCachedColors];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@end
|
|
242
|
+
|
|
243
|
+
Class<RCTComponentViewProtocol> NanoIconViewCls(void)
|
|
244
|
+
{
|
|
245
|
+
return NanoIconView.class;
|
|
246
|
+
}
|
|
@@ -20,7 +20,7 @@ function shouldSkipGeneration(inputHash, outputDir, fontFamily, logger) {
|
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
const glyphmap = JSON.parse(fs_1.default.readFileSync(glyphmapPath, 'utf8'));
|
|
23
|
-
const storedHash = glyphmap?.
|
|
23
|
+
const storedHash = glyphmap?.m?.h;
|
|
24
24
|
if (storedHash && storedHash === inputHash) {
|
|
25
25
|
logger?.info(`${fontFamily}: SVG fingerprint unchanged, skipping build.`);
|
|
26
26
|
return true;
|
|
@@ -3,7 +3,7 @@ export type NanoIconsConfig = {
|
|
|
3
3
|
iconSets: IconSetConfig[];
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
|
-
* Load .nanoicons.json from the
|
|
6
|
+
* Load .nanoicons.json from the given directory.
|
|
7
7
|
* Throws with a helpful message if the file is missing or malformed.
|
|
8
8
|
*/
|
|
9
|
-
export declare function loadNanoIconsConfig(
|
|
9
|
+
export declare function loadNanoIconsConfig(configRoot: string): NanoIconsConfig;
|