@geastack/apple 0.2.8

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.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +54 -0
  3. package/dist/index.d.ts +127 -0
  4. package/dist/index.js +3076 -0
  5. package/generated/AVFoundation.d.ts +112 -0
  6. package/generated/ActivityKit.d.ts +7 -0
  7. package/generated/AppKit.d.ts +268 -0
  8. package/generated/CoreGraphics.d.ts +18 -0
  9. package/generated/CoreLocation.d.ts +28 -0
  10. package/generated/CoreMedia.d.ts +8 -0
  11. package/generated/Dispatch.d.ts +2 -0
  12. package/generated/Foundation.d.ts +13 -0
  13. package/generated/MapKit.d.ts +28 -0
  14. package/generated/Metal.d.ts +91 -0
  15. package/generated/MetalKit.d.ts +22 -0
  16. package/generated/Photos.d.ts +5 -0
  17. package/generated/QuartzCore.d.ts +12 -0
  18. package/generated/SwiftData.d.ts +11 -0
  19. package/generated/UIKit.d.ts +187 -0
  20. package/package.json +123 -0
  21. package/runtime/AVFoundation.js +136 -0
  22. package/runtime/ActivityKit.js +18 -0
  23. package/runtime/AppKit.js +313 -0
  24. package/runtime/CoreGraphics.js +15 -0
  25. package/runtime/CoreLocation.js +30 -0
  26. package/runtime/CoreMedia.js +7 -0
  27. package/runtime/Dispatch.js +11 -0
  28. package/runtime/Foundation.js +21 -0
  29. package/runtime/MapKit.js +22 -0
  30. package/runtime/Metal.js +126 -0
  31. package/runtime/MetalKit.js +15 -0
  32. package/runtime/Photos.js +9 -0
  33. package/runtime/QuartzCore.js +9 -0
  34. package/runtime/SwiftData.js +29 -0
  35. package/runtime/UIKit.js +242 -0
  36. package/runtime/empty.js +1 -0
  37. package/targets/ios/Info.plist.in +25 -0
  38. package/targets/ios/README.md +46 -0
  39. package/targets/ios/build-ios.sh +395 -0
  40. package/targets/ios/detect-development-team.mjs +109 -0
  41. package/targets/ios/generate-resident-entry.mjs +67 -0
  42. package/targets/ios/generate-resident-registry.mjs +162 -0
  43. package/targets/ios/generate-xcode-project.mjs +520 -0
  44. package/targets/ios/main/canvas_view.h +7 -0
  45. package/targets/ios/main/canvas_view.mm +94 -0
  46. package/targets/ios/main/font_registry.h +17 -0
  47. package/targets/ios/main/font_registry.mm +129 -0
  48. package/targets/ios/main/image_bridge.h +13 -0
  49. package/targets/ios/main/image_bridge.mm +53 -0
  50. package/targets/ios/main/ios_apps.c +28 -0
  51. package/targets/ios/main/ios_camera.mm +446 -0
  52. package/targets/ios/main/ios_display.mm +344 -0
  53. package/targets/ios/main/ios_main.mm +811 -0
  54. package/targets/ios/main/ios_memory.cpp +55 -0
  55. package/targets/ios/main/ios_renderer.h +20 -0
  56. package/targets/ios/main/ios_renderer.mm +27 -0
  57. package/targets/ios/main/ios_root_background.cpp +20 -0
  58. package/targets/ios/main/ios_root_background.h +11 -0
  59. package/targets/ios/main/ios_sensors.mm +244 -0
  60. package/targets/ios/main/ios_timers.mm +53 -0
  61. package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
  62. package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
  63. package/targets/ios/main/renderer/native_button.mm +179 -0
  64. package/targets/ios/main/renderer/native_label.mm +161 -0
  65. package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
  66. package/targets/ios/main/renderer/view_reconciler.mm +274 -0
  67. package/targets/macos/Info.plist.in +26 -0
  68. package/targets/macos/README.md +131 -0
  69. package/targets/macos/build-child-lifecycle.sh +119 -0
  70. package/targets/macos/build-macos.sh +2156 -0
  71. package/targets/macos/check-module-graph-freshness.mjs +82 -0
  72. package/targets/macos/compiler-input-fingerprint.mjs +32 -0
  73. package/targets/macos/finalize-apple-native-output.mjs +77 -0
  74. package/targets/macos/generate-resident-entry.mjs +80 -0
  75. package/targets/macos/generate-resident-registry.mjs +141 -0
  76. package/targets/macos/heavy-build-lock.sh +176 -0
  77. package/targets/macos/main/canvas_view.h +9 -0
  78. package/targets/macos/main/canvas_view.mm +176 -0
  79. package/targets/macos/main/color_convert.h +18 -0
  80. package/targets/macos/main/color_convert.mm +39 -0
  81. package/targets/macos/main/font_registry.h +35 -0
  82. package/targets/macos/main/font_registry.mm +350 -0
  83. package/targets/macos/main/image_bridge.h +16 -0
  84. package/targets/macos/main/image_bridge.mm +52 -0
  85. package/targets/macos/main/macos_app_platform.mm +95 -0
  86. package/targets/macos/main/macos_apps.c +31 -0
  87. package/targets/macos/main/macos_device_control.mm +56 -0
  88. package/targets/macos/main/macos_display.mm +484 -0
  89. package/targets/macos/main/macos_host_device_control.mm +95 -0
  90. package/targets/macos/main/macos_main.mm +839 -0
  91. package/targets/macos/main/macos_memory.cpp +31 -0
  92. package/targets/macos/main/macos_native_shell.h +31 -0
  93. package/targets/macos/main/macos_native_shell.mm +396 -0
  94. package/targets/macos/main/macos_network.mm +253 -0
  95. package/targets/macos/main/macos_renderer.h +39 -0
  96. package/targets/macos/main/macos_renderer.mm +1969 -0
  97. package/targets/macos/main/macos_sensors.cpp +148 -0
  98. package/targets/macos/main/macos_smoke_app.cpp +65 -0
  99. package/targets/macos/main/macos_timers.mm +52 -0
  100. package/targets/macos/main/press_bridge.h +21 -0
  101. package/targets/macos/main/press_bridge.mm +72 -0
  102. package/targets/macos/main/thermalright_hid_display.h +14 -0
  103. package/targets/macos/main/thermalright_hid_display.mm +995 -0
  104. package/targets/macos/native-artifact-provenance.mjs +421 -0
  105. package/targets/macos/order-compile-edges.mjs +64 -0
  106. package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
  107. package/targets/macos/prune-generated-modules.mjs +147 -0
  108. package/targets/macos/sync-resources.mjs +153 -0
  109. package/targets/shared/analyze-audio-capture.mjs +131 -0
  110. package/targets/shared/apple_audio.mm +1194 -0
  111. package/targets/shared/apple_audio_selftest.mm +602 -0
  112. package/targets/shared/apple_audio_testing.h +34 -0
  113. package/targets/shared/build-audio-selftest.sh +50 -0
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import { execFileSync } from 'node:child_process'
8
+
9
+ // The project is the directory this ran in -- the one being compiled, which is
10
+ // also what the CLI resolves from cwd. iOS target sources live next to this
11
+ // script, not in the project. App metadata and icon generation come from the
12
+ // gea CLI (build-ios.sh resolves it and passes $GEA_CLI_BIN).
13
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
14
+ const projectDir = process.cwd()
15
+ const geaCli = process.env.GEA_CLI_BIN || path.join(scriptDir, '../../node_modules/@geastack/cli/bin/gea.mjs')
16
+ function gea(args) {
17
+ return execFileSync(process.execPath, [geaCli, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] })
18
+ }
19
+
20
+ const args = process.argv.slice(2)
21
+
22
+ function opt(name) {
23
+ const i = args.indexOf(name)
24
+ return i >= 0 ? args[i + 1] : ''
25
+ }
26
+
27
+ function fail(message) {
28
+ process.stderr.write(`${message}\n`)
29
+ process.exit(1)
30
+ }
31
+
32
+ const appId = opt('--app-id')
33
+ const appName = opt('--app-name')
34
+ const bundleId = opt('--bundle-id')
35
+ const generatedDir = path.resolve(opt('--generated-dir'))
36
+ const projectPath = path.resolve(opt('--project-path'))
37
+
38
+ if (!appId || !appName || !bundleId || !generatedDir || !projectPath) {
39
+ fail('usage: generate-xcode-project.mjs --app-id <id> --app-name <name> --bundle-id <id> --generated-dir <path> --project-path <path> (run from the app directory)')
40
+ }
41
+
42
+ const iosDir = scriptDir // apple/targets/ios — this script's own directory, not the apps repo
43
+ const plistOut = path.join(projectPath, 'Info.plist')
44
+ let appMeta
45
+ try {
46
+ appMeta = JSON.parse(gea(['apps', 'inspect', appId, '--json']))
47
+ } catch {
48
+ fail(`Unknown app id: ${appId}`)
49
+ }
50
+ // `apps inspect` reports an absolute directory; there is nothing to join it to.
51
+ const appRoot = appMeta.root
52
+ const appIconAssetsDir = path.join(generatedDir, 'Assets.xcassets')
53
+
54
+ // Omit the app-icon asset catalog when GEA_IOS_SKIP_APP_ICON=1. actool refuses to
55
+ // compile a catalog unless an installed simulator runtime's build matches the SDK
56
+ // exactly (e.g. SDK 23F81a needs a 23F81a runtime, not a 23F73 one) — a skew that
57
+ // blocks otherwise-fine builds. Skipping it yields an app with the default icon.
58
+ const skipAppIcon = process.env.GEA_IOS_SKIP_APP_ICON === '1'
59
+
60
+ fs.rmSync(projectPath, { recursive: true, force: true })
61
+ fs.mkdirSync(path.join(projectPath, 'project.xcworkspace'), { recursive: true })
62
+ fs.mkdirSync(path.join(projectPath, 'xcshareddata/xcschemes'), { recursive: true })
63
+ if (!skipAppIcon) gea(['apps', 'apple-icons', appId, '--platform', 'ios', '--assets-dir', appIconAssetsDir])
64
+
65
+ const plistTemplate = fs.readFileSync(path.join(iosDir, 'Info.plist.in'), 'utf8')
66
+ fs.writeFileSync(
67
+ plistOut,
68
+ plistTemplate
69
+ .replaceAll('@APP_EXEC@', appName)
70
+ .replaceAll('@APP_NAME@', appName)
71
+ .replaceAll('@BUNDLE_ID@', bundleId),
72
+ )
73
+
74
+ function id(label) {
75
+ return crypto.createHash('sha1').update(label).digest('hex').slice(0, 24).toUpperCase()
76
+ }
77
+
78
+ function q(value) {
79
+ return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`
80
+ }
81
+
82
+ function rel(file) {
83
+ return path.relative(projectDir, file).replaceAll(path.sep, '/')
84
+ }
85
+
86
+ function source(file, kind = 'sourcecode.cpp.cpp') {
87
+ return { file: path.resolve(file), kind }
88
+ }
89
+
90
+ function resource(file, kind = 'file') {
91
+ return { file: path.resolve(file), kind }
92
+ }
93
+
94
+ function existing(entry) {
95
+ return fs.existsSync(entry.file)
96
+ }
97
+
98
+ function readGeatscSources(dir, kind = 'sourcecode.cpp.cpp') {
99
+ const sourceList = path.join(dir, 'geatsc-sources.txt')
100
+ if (!fs.existsSync(sourceList)) return []
101
+ return fs.readFileSync(sourceList, 'utf8')
102
+ .split(/\r?\n/)
103
+ .map((line) => line.trim())
104
+ .filter(Boolean)
105
+ .map((file) => source(file, kind))
106
+ .filter(existing)
107
+ }
108
+
109
+ function nativeSourceKind(file) {
110
+ if (/\.mm$/i.test(file)) return 'sourcecode.cpp.objcpp'
111
+ if (/\.m$/i.test(file)) return 'sourcecode.c.objc'
112
+ if (/\.c$/i.test(file)) return 'sourcecode.c.c'
113
+ return 'sourcecode.cpp.cpp'
114
+ }
115
+
116
+ function findAppNativeSources() {
117
+ const dir = path.join(appRoot, 'ios')
118
+ if (!fs.existsSync(dir)) return []
119
+ const out = []
120
+ const visit = (current) => {
121
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
122
+ const file = path.join(current, entry.name)
123
+ if (entry.isDirectory()) {
124
+ visit(file)
125
+ } else if (entry.isFile() && /\.(c|cc|cpp|m|mm)$/i.test(entry.name)) {
126
+ out.push(source(file, nativeSourceKind(file)))
127
+ }
128
+ }
129
+ }
130
+ visit(dir)
131
+ return out.sort((a, b) => a.file.localeCompare(b.file))
132
+ }
133
+
134
+ function findFontResources() {
135
+ const fontDir = path.join(projectDir, 'assets/fonts')
136
+ if (!fs.existsSync(fontDir)) return []
137
+ return fs.readdirSync(fontDir, { withFileTypes: true })
138
+ .filter((entry) => entry.isFile() && /\.(ttf|otf)$/i.test(entry.name))
139
+ .map((entry) => path.join(fontDir, entry.name))
140
+ .sort()
141
+ }
142
+
143
+ function findResidentSources() {
144
+ const residentsDir = path.join(generatedDir, 'residents')
145
+ if (!fs.existsSync(residentsDir)) return []
146
+ return fs.readdirSync(residentsDir, { withFileTypes: true })
147
+ .filter((entry) => entry.isDirectory())
148
+ .flatMap((entry) => {
149
+ const dir = path.join(residentsDir, entry.name)
150
+ return [
151
+ ...readGeatscSources(dir),
152
+ source(path.join(dir, 'entry.cpp')),
153
+ source(path.join(dir, 'gea_embedded_font_generated.cpp')),
154
+ source(path.join(dir, 'gea_embedded_assets_generated.cpp')),
155
+ ].filter(existing)
156
+ })
157
+ }
158
+
159
+ const residentRegistry = source(path.join(generatedDir, 'resident_registry.cpp'))
160
+ const hasResidentRegistry = existing(residentRegistry)
161
+ const residentSources = hasResidentRegistry ? [residentRegistry, ...findResidentSources()] : []
162
+ const appleNativeBridgeSource = source(path.join(generatedDir, 'gea/apple/native_bridge.mm'), 'sourcecode.cpp.objcpp')
163
+ const generatedProgramSources = readGeatscSources(
164
+ generatedDir,
165
+ existing(appleNativeBridgeSource) ? 'sourcecode.cpp.objcpp' : 'sourcecode.cpp.cpp',
166
+ )
167
+ const rendererSources = [
168
+ 'ios_renderer.mm',
169
+ 'renderer/ios_renderer_support.mm',
170
+ 'renderer/native_label.mm',
171
+ 'renderer/native_button.mm',
172
+ 'renderer/native_scroll_container.mm',
173
+ 'renderer/view_reconciler.mm',
174
+ ].map((file) => source(path.join(iosDir, 'main', file), 'sourcecode.cpp.objcpp'))
175
+ const appNativeSources = findAppNativeSources()
176
+ const sources = [
177
+ source(path.join(iosDir, 'main/ios_main.mm'), 'sourcecode.cpp.objcpp'),
178
+ source(path.join(iosDir, 'main/ios_root_background.cpp')),
179
+ ...rendererSources,
180
+ source(path.join(iosDir, 'main/canvas_view.mm'), 'sourcecode.cpp.objcpp'),
181
+ source(path.join(iosDir, 'main/color_convert.mm'), 'sourcecode.cpp.objcpp'),
182
+ source(path.join(iosDir, 'main/image_bridge.mm'), 'sourcecode.cpp.objcpp'),
183
+ source(path.join(iosDir, 'main/font_registry.mm'), 'sourcecode.cpp.objcpp'),
184
+ source(path.join(iosDir, 'main/press_bridge.mm'), 'sourcecode.cpp.objcpp'),
185
+ source(path.join(iosDir, 'main/ios_display.mm'), 'sourcecode.cpp.objcpp'),
186
+ source(path.join(iosDir, 'main/ios_timers.mm'), 'sourcecode.cpp.objcpp'),
187
+ source(path.join(iosDir, 'main/ios_app_platform.mm'), 'sourcecode.cpp.objcpp'),
188
+ source(path.join(iosDir, 'main/ios_memory.cpp')),
189
+ // gea::platform::audio — the AVAudioEngine backend, shared verbatim with macOS.
190
+ source(path.join(iosDir, '../shared/apple_audio.mm'), 'sourcecode.cpp.objcpp'),
191
+ source(path.join(iosDir, 'main/ios_sensors.mm'), 'sourcecode.cpp.objcpp'),
192
+ source(path.join(iosDir, 'main/ios_camera.mm'), 'sourcecode.cpp.objcpp'),
193
+ source(path.join(iosDir, 'main/ios_apps.c'), 'sourcecode.c.c'),
194
+ ...appNativeSources,
195
+ // Framework C/C++ sources come from @geastack/core/gea_sources.sh,
196
+ // passed by build-ios.sh via env (iOS-curated: camera kept since ios_camera.mm
197
+ // provides it; resident-app/OTA/diagnostics services + the single-app runtime
198
+ // shell excluded — iOS uses its own UIKit run loop). Single source of truth.
199
+ ...(process.env.GEA_FW_C_SOURCES || '').split('\n').filter(Boolean).map((f) => source(f, 'sourcecode.c.c')),
200
+ ...(process.env.GEA_FW_CXX_SOURCES || '').split('\n').filter(Boolean).map((f) => source(f)),
201
+ // gea_app_entry / resident_apps are mode-specific (deliberately not in the manifest).
202
+ ...(hasResidentRegistry
203
+ ? []
204
+ : [
205
+ source(path.join(geaCore, 'gea_app_entry.cpp')),
206
+ source(path.join(geaCore, '../geaos/resident_apps.cpp')),
207
+ ]),
208
+ ...(hasResidentRegistry ? residentSources : generatedProgramSources),
209
+ appleNativeBridgeSource,
210
+ ].filter(existing)
211
+
212
+ const fontGenerated = source(path.join(generatedDir, 'gea_embedded_font_generated.cpp'))
213
+ if (!hasResidentRegistry && existing(fontGenerated)) sources.push(fontGenerated)
214
+ const assetsGenerated = source(path.join(generatedDir, 'gea_embedded_assets_generated.cpp'))
215
+ if (!hasResidentRegistry && existing(assetsGenerated)) sources.push(assetsGenerated)
216
+ const hasGeneratedFonts = sources.some((entry) => path.basename(entry.file) === 'gea_embedded_font_generated.cpp')
217
+
218
+ const fontResources = findFontResources().map((file) => resource(file))
219
+ const appIconAssets = resource(appIconAssetsDir, 'folder.assetcatalog')
220
+ const appIconResources = skipAppIcon ? [] : [appIconAssets]
221
+ const uiTestSource = source(path.join(iosDir, 'test/GeaIosRubberBandUITests.swift'), 'sourcecode.swift')
222
+ const uiTestSources = existing(uiTestSource) ? [uiTestSource] : []
223
+ const hasUiTests = uiTestSources.length > 0
224
+
225
+ const frameworks = [
226
+ 'UIKit.framework',
227
+ 'QuartzCore.framework',
228
+ 'CoreGraphics.framework',
229
+ 'CoreText.framework',
230
+ 'CoreMotion.framework',
231
+ 'CoreLocation.framework',
232
+ 'CoreMedia.framework',
233
+ 'CoreVideo.framework',
234
+ 'ImageIO.framework',
235
+ 'MapKit.framework',
236
+ 'AVFoundation.framework',
237
+ 'Photos.framework',
238
+ 'Metal.framework',
239
+ 'MetalKit.framework',
240
+ 'Foundation.framework',
241
+ ]
242
+
243
+ const productId = id('product')
244
+ const targetId = id('target')
245
+ const projectId = id('project')
246
+ const mainGroupId = id('group-main')
247
+ const productGroupId = id('group-products')
248
+ const sourcesPhaseId = id('phase-sources')
249
+ const frameworksPhaseId = id('phase-frameworks')
250
+ const resourcesPhaseId = id('phase-resources')
251
+ const configListProjectId = id('configs-project')
252
+ const configListTargetId = id('configs-target')
253
+ const debugProjectId = id('debug-project')
254
+ const releaseProjectId = id('release-project')
255
+ const debugTargetId = id('debug-target')
256
+ const releaseTargetId = id('release-target')
257
+ const uiTestProductId = id('product-ui-tests')
258
+ const uiTestTargetId = id('target-ui-tests')
259
+ const uiTestSourcesPhaseId = id('phase-ui-test-sources')
260
+ const uiTestFrameworksPhaseId = id('phase-ui-test-frameworks')
261
+ const uiTestConfigListId = id('configs-ui-test-target')
262
+ const uiTestDebugId = id('debug-ui-test-target')
263
+ const uiTestReleaseId = id('release-ui-test-target')
264
+ const uiTestDependencyId = id('dependency-ui-test-app')
265
+ const uiTestProxyId = id('proxy-ui-test-app')
266
+
267
+ const fileRefs = []
268
+ const buildFiles = []
269
+
270
+ for (const s of sources) {
271
+ const label = rel(s.file)
272
+ const compilerSettings = s.kind === 'sourcecode.cpp.objcpp'
273
+ ? ' settings = {COMPILER_FLAGS = "-fobjc-exceptions"; };'
274
+ : ''
275
+ fileRefs.push(`${id(`file:${label}`)} /* ${path.basename(s.file)} */ = {isa = PBXFileReference; lastKnownFileType = ${s.kind}; path = ${q(s.file)}; sourceTree = "<absolute>"; };`)
276
+ buildFiles.push(`${id(`build:${label}`)} /* ${path.basename(s.file)} in Sources */ = {isa = PBXBuildFile; fileRef = ${id(`file:${label}`)} /* ${path.basename(s.file)} */;${compilerSettings} };`)
277
+ }
278
+
279
+ for (const r of fontResources) {
280
+ const label = rel(r.file)
281
+ fileRefs.push(`${id(`file:${label}`)} /* ${path.basename(r.file)} */ = {isa = PBXFileReference; lastKnownFileType = ${r.kind}; path = ${q(r.file)}; sourceTree = "<absolute>"; };`)
282
+ buildFiles.push(`${id(`build:${label}`)} /* ${path.basename(r.file)} in Resources */ = {isa = PBXBuildFile; fileRef = ${id(`file:${label}`)} /* ${path.basename(r.file)} */; };`)
283
+ }
284
+
285
+ for (const a of appIconResources) {
286
+ const label = rel(a.file)
287
+ fileRefs.push(`${id(`file:${label}`)} /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = ${a.kind}; path = ${q(a.file)}; sourceTree = "<absolute>"; };`)
288
+ buildFiles.push(`${id(`build:${label}`)} /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ${id(`file:${label}`)} /* Assets.xcassets */; };`)
289
+ }
290
+
291
+ for (const fw of frameworks) {
292
+ fileRefs.push(`${id(`file:${fw}`)} /* ${fw} */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ${fw}; path = System/Library/Frameworks/${fw}; sourceTree = SDKROOT; };`)
293
+ buildFiles.push(`${id(`build:${fw}`)} /* ${fw} in Frameworks */ = {isa = PBXBuildFile; fileRef = ${id(`file:${fw}`)} /* ${fw} */; };`)
294
+ }
295
+
296
+ for (const s of uiTestSources) {
297
+ const label = rel(s.file)
298
+ fileRefs.push(`${id(`file:${label}`)} /* ${path.basename(s.file)} */ = {isa = PBXFileReference; lastKnownFileType = ${s.kind}; path = ${q(s.file)}; sourceTree = "<absolute>"; };`)
299
+ buildFiles.push(`${id(`build:${label}`)} /* ${path.basename(s.file)} in Sources */ = {isa = PBXBuildFile; fileRef = ${id(`file:${label}`)} /* ${path.basename(s.file)} */; };`)
300
+ }
301
+ if (hasUiTests) {
302
+ fileRefs.push(`${id('file:XCTest.framework')} /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = System/Library/Frameworks/XCTest.framework; sourceTree = SDKROOT; };`)
303
+ buildFiles.push(`${id('build:XCTest.framework')} /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ${id('file:XCTest.framework')} /* XCTest.framework */; };`)
304
+ }
305
+
306
+ const productRef = `${productId} /* ${appName}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ${q(`${appName}.app`)}; sourceTree = BUILT_PRODUCTS_DIR; };`
307
+ const uiTestProductRef = hasUiTests
308
+ ? `${uiTestProductId} /* GeaIosRubberBandUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GeaIosRubberBandUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };`
309
+ : ''
310
+ const includePaths = [
311
+ path.join(iosDir, 'main'),
312
+ generatedDir,
313
+ path.join(appRoot, 'ios'),
314
+ // Framework include roots (core/host/engine includes + vendor/stb + AnimatedGIF)
315
+ // from the shared manifest gea_sources.sh, passed by build-ios.sh via env.
316
+ ...(process.env.GEA_FW_INCLUDE_DIRS || '').split('\n').filter(Boolean),
317
+ ]
318
+
319
+ function buildSettings(configuration) {
320
+ const preprocessorDefinitions = [
321
+ 'GEA_EMBEDDED_GIF_C_API=1',
322
+ 'GEA_EMBEDDED_ENABLE_NATIVE_TEXT_INPUT=1',
323
+ 'GEA_EMBEDDED_ENABLE_VIRTUAL_KEYBOARD=0',
324
+ 'GEA_CPP_USE_TO_CHARS_DOUBLE=0',
325
+ 'GEA_CPP_USE_FROM_CHARS_DOUBLE=0',
326
+ // iOS renders full colour: pixel::native_t is RGBA8888 (32-bit), not RGB565.
327
+ // 1 == GEA_PIXEL_RGBA8888 (see pixel.h). The Canvas engine, the primary
328
+ // display path (ios_display.mm's uint32 framebuffer → gea_ios_display_presented_pixels),
329
+ // and the shared text/canvas-store/camera paths all operate on the 32-bit
330
+ // native pixel with no runtime format branch and no colour conversion.
331
+ 'GEA_EMBEDDED_PIXEL_FORMAT=1',
332
+ ...(hasGeneratedFonts ? ['GEA_EMBEDDED_HAS_GENERATED_FONTS=1'] : []),
333
+ ]
334
+ return `{
335
+ ALWAYS_SEARCH_USER_PATHS = NO;${skipAppIcon ? '' : `
336
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;`}
337
+ CLANG_CXX_LANGUAGE_STANDARD = "c++20";
338
+ CLANG_CXX_LIBRARY = "libc++";
339
+ CLANG_ENABLE_MODULES = YES;
340
+ CLANG_ENABLE_OBJC_ARC = YES;
341
+ CODE_SIGN_STYLE = Automatic;
342
+ CURRENT_PROJECT_VERSION = 1;
343
+ DEFINES_MODULE = NO;
344
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
345
+ GCC_PREPROCESSOR_DEFINITIONS = (${preprocessorDefinitions.map(q).join(', ')});
346
+ HEADER_SEARCH_PATHS = (${includePaths.map(q).join(', ')});
347
+ INFOPLIST_FILE = ${q(plistOut)};
348
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
349
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
350
+ ONLY_ACTIVE_ARCH = ${configuration === 'Debug' ? 'YES' : 'NO'};
351
+ PRODUCT_BUNDLE_IDENTIFIER = ${q(bundleId)};
352
+ PRODUCT_NAME = ${q(appName)};
353
+ SDKROOT = iphoneos;
354
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
355
+ SUPPORTS_MACCATALYST = NO;
356
+ TARGETED_DEVICE_FAMILY = "1,2";
357
+ }`
358
+ }
359
+
360
+ function uiTestBuildSettings(configuration) {
361
+ return `{
362
+ ALWAYS_SEARCH_USER_PATHS = NO;
363
+ CLANG_ENABLE_MODULES = YES;
364
+ CODE_SIGN_STYLE = Automatic;
365
+ GENERATE_INFOPLIST_FILE = YES;
366
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
367
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
368
+ ONLY_ACTIVE_ARCH = ${configuration === 'Debug' ? 'YES' : 'NO'};
369
+ PRODUCT_BUNDLE_IDENTIFIER = ${q(`${bundleId}.rubber-band-ui-tests`)};
370
+ PRODUCT_MODULE_NAME = GeaIosRubberBandUITests;
371
+ PRODUCT_NAME = GeaIosRubberBandUITests;
372
+ SDKROOT = iphoneos;
373
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
374
+ SUPPORTS_MACCATALYST = NO;
375
+ SWIFT_VERSION = 5.0;
376
+ TARGETED_DEVICE_FAMILY = "1,2";
377
+ TEST_TARGET_NAME = GeaIos;
378
+ }`
379
+ }
380
+
381
+ const sourceChildren = sources.map((s) => `${id(`file:${rel(s.file)}`)} /* ${path.basename(s.file)} */`)
382
+ const uiTestSourceChildren = uiTestSources.map((s) => `${id(`file:${rel(s.file)}`)} /* ${path.basename(s.file)} */`)
383
+ const resourceChildren = [
384
+ ...fontResources.map((r) => `${id(`file:${rel(r.file)}`)} /* ${path.basename(r.file)} */`),
385
+ ...appIconResources.map((a) => `${id(`file:${rel(a.file)}`)} /* Assets.xcassets */`),
386
+ ]
387
+ const frameworkChildren = frameworks.map((fw) => `${id(`file:${fw}`)} /* ${fw} */`)
388
+ const mainGroupChildren = [
389
+ ...sourceChildren,
390
+ ...uiTestSourceChildren,
391
+ ...resourceChildren,
392
+ ...frameworkChildren,
393
+ ...(hasUiTests ? [`${id('file:XCTest.framework')} /* XCTest.framework */`] : []),
394
+ `${productGroupId} /* Products */`,
395
+ ]
396
+ const sourceBuildRefs = sources.map((s) => `${id(`build:${rel(s.file)}`)} /* ${path.basename(s.file)} in Sources */`)
397
+ const uiTestSourceBuildRefs = uiTestSources.map((s) => `${id(`build:${rel(s.file)}`)} /* ${path.basename(s.file)} in Sources */`)
398
+ const resourceBuildRefs = [
399
+ ...fontResources.map((r) => `${id(`build:${rel(r.file)}`)} /* ${path.basename(r.file)} in Resources */`),
400
+ ...appIconResources.map((a) => `${id(`build:${rel(a.file)}`)} /* Assets.xcassets in Resources */`),
401
+ ]
402
+ const frameworkBuildRefs = frameworks.map((fw) => `${id(`build:${fw}`)} /* ${fw} in Frameworks */`)
403
+ const uiTestFrameworkBuildRefs = hasUiTests ? [`${id('build:XCTest.framework')} /* XCTest.framework in Frameworks */`] : []
404
+
405
+ const pbxproj = `// !$*UTF8*$!
406
+ {
407
+ archiveVersion = 1;
408
+ classes = {};
409
+ objectVersion = 56;
410
+ objects = {
411
+
412
+ /* Begin PBXBuildFile section */
413
+ ${buildFiles.join('\n\t\t')}
414
+ /* End PBXBuildFile section */
415
+
416
+ /* Begin PBXFileReference section */
417
+ ${fileRefs.join('\n\t\t')}
418
+ ${productRef}
419
+ ${uiTestProductRef}
420
+ /* End PBXFileReference section */
421
+
422
+ /* Begin PBXFrameworksBuildPhase section */
423
+ ${frameworksPhaseId} = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (${frameworkBuildRefs.join(', ')}); runOnlyForDeploymentPostprocessing = 0; };
424
+ ${hasUiTests ? `${uiTestFrameworksPhaseId} = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (${uiTestFrameworkBuildRefs.join(', ')}); runOnlyForDeploymentPostprocessing = 0; };` : ''}
425
+ /* End PBXFrameworksBuildPhase section */
426
+
427
+ /* Begin PBXGroup section */
428
+ ${mainGroupId} = {isa = PBXGroup; children = (${mainGroupChildren.join(', ')}); sourceTree = "<group>"; };
429
+ ${productGroupId} = {isa = PBXGroup; children = (${[`${productId} /* ${appName}.app */`, ...(hasUiTests ? [`${uiTestProductId} /* GeaIosRubberBandUITests.xctest */`] : [])].join(', ')}); name = Products; sourceTree = "<group>"; };
430
+ /* End PBXGroup section */
431
+
432
+ ${hasUiTests ? `/* Begin PBXContainerItemProxy section */
433
+ ${uiTestProxyId} = {isa = PBXContainerItemProxy; containerPortal = ${projectId} /* Project object */; proxyType = 1; remoteGlobalIDString = ${targetId}; remoteInfo = GeaIos; };
434
+ /* End PBXContainerItemProxy section */` : ''}
435
+
436
+ /* Begin PBXNativeTarget section */
437
+ ${targetId} = {isa = PBXNativeTarget; buildConfigurationList = ${configListTargetId}; buildPhases = (${sourcesPhaseId}, ${frameworksPhaseId}, ${resourcesPhaseId}); buildRules = (); dependencies = (); name = GeaIos; productName = ${q(appName)}; productReference = ${productId} /* ${appName}.app */; productType = "com.apple.product-type.application"; };
438
+ ${hasUiTests ? `${uiTestTargetId} = {isa = PBXNativeTarget; buildConfigurationList = ${uiTestConfigListId}; buildPhases = (${uiTestSourcesPhaseId}, ${uiTestFrameworksPhaseId}); buildRules = (); dependencies = (${uiTestDependencyId}); name = GeaIosRubberBandUITests; productName = GeaIosRubberBandUITests; productReference = ${uiTestProductId} /* GeaIosRubberBandUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; };` : ''}
439
+ /* End PBXNativeTarget section */
440
+
441
+ /* Begin PBXProject section */
442
+ ${projectId} = {isa = PBXProject; attributes = {LastUpgradeCheck = 1600; TargetAttributes = {${targetId} = {CreatedOnToolsVersion = 16.0;};${hasUiTests ? ` ${uiTestTargetId} = {CreatedOnToolsVersion = 16.0; TestTargetID = ${targetId};};` : ''}};}; buildConfigurationList = ${configListProjectId}; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = (en, Base); mainGroup = ${mainGroupId}; productRefGroup = ${productGroupId}; projectDirPath = ""; projectRoot = ""; targets = (${[targetId, ...(hasUiTests ? [uiTestTargetId] : [])].join(', ')}); };
443
+ /* End PBXProject section */
444
+
445
+ /* Begin PBXResourcesBuildPhase section */
446
+ ${resourcesPhaseId} = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (${resourceBuildRefs.join(', ')}); runOnlyForDeploymentPostprocessing = 0; };
447
+ /* End PBXResourcesBuildPhase section */
448
+
449
+ /* Begin PBXSourcesBuildPhase section */
450
+ ${sourcesPhaseId} = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (${sourceBuildRefs.join(', ')}); runOnlyForDeploymentPostprocessing = 0; };
451
+ ${hasUiTests ? `${uiTestSourcesPhaseId} = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (${uiTestSourceBuildRefs.join(', ')}); runOnlyForDeploymentPostprocessing = 0; };` : ''}
452
+ /* End PBXSourcesBuildPhase section */
453
+
454
+ ${hasUiTests ? `/* Begin PBXTargetDependency section */
455
+ ${uiTestDependencyId} = {isa = PBXTargetDependency; target = ${targetId} /* GeaIos */; targetProxy = ${uiTestProxyId}; };
456
+ /* End PBXTargetDependency section */` : ''}
457
+
458
+ /* Begin XCBuildConfiguration section */
459
+ ${debugProjectId} = {isa = XCBuildConfiguration; buildSettings = {}; name = Debug; };
460
+ ${releaseProjectId} = {isa = XCBuildConfiguration; buildSettings = {}; name = Release; };
461
+ ${debugTargetId} = {isa = XCBuildConfiguration; buildSettings = ${buildSettings('Debug')}; name = Debug; };
462
+ ${releaseTargetId} = {isa = XCBuildConfiguration; buildSettings = ${buildSettings('Release')}; name = Release; };
463
+ ${hasUiTests ? `${uiTestDebugId} = {isa = XCBuildConfiguration; buildSettings = ${uiTestBuildSettings('Debug')}; name = Debug; };
464
+ ${uiTestReleaseId} = {isa = XCBuildConfiguration; buildSettings = ${uiTestBuildSettings('Release')}; name = Release; };` : ''}
465
+ /* End XCBuildConfiguration section */
466
+
467
+ /* Begin XCConfigurationList section */
468
+ ${configListProjectId} = {isa = XCConfigurationList; buildConfigurations = (${debugProjectId}, ${releaseProjectId}); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; };
469
+ ${configListTargetId} = {isa = XCConfigurationList; buildConfigurations = (${debugTargetId}, ${releaseTargetId}); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; };
470
+ ${hasUiTests ? `${uiTestConfigListId} = {isa = XCConfigurationList; buildConfigurations = (${uiTestDebugId}, ${uiTestReleaseId}); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; };` : ''}
471
+ /* End XCConfigurationList section */
472
+ };
473
+ rootObject = ${projectId};
474
+ }
475
+ `
476
+
477
+ const scheme = `<?xml version="1.0" encoding="UTF-8"?>
478
+ <Scheme LastUpgradeVersion="1600" version="1.7">
479
+ <BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES" buildArchitectures="Automatic">
480
+ <BuildActionEntries>
481
+ <BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
482
+ <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="${targetId}" BuildableName="${appName}.app" BlueprintName="GeaIos" ReferencedContainer="container:GeaIos.xcodeproj"/>
483
+ </BuildActionEntry>
484
+ ${hasUiTests ? `<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
485
+ <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="${uiTestTargetId}" BuildableName="GeaIosRubberBandUITests.xctest" BlueprintName="GeaIosRubberBandUITests" ReferencedContainer="container:GeaIos.xcodeproj"/>
486
+ </BuildActionEntry>` : ''}
487
+ </BuildActionEntries>
488
+ </BuildAction>
489
+ ${hasUiTests ? `<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES" shouldAutocreateTestPlan="YES">
490
+ <Testables>
491
+ <TestableReference skipped="NO">
492
+ <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="${uiTestTargetId}" BuildableName="GeaIosRubberBandUITests.xctest" BlueprintName="GeaIosRubberBandUITests" ReferencedContainer="container:GeaIos.xcodeproj"/>
493
+ </TestableReference>
494
+ </Testables>
495
+ <MacroExpansion>
496
+ <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="${targetId}" BuildableName="${appName}.app" BlueprintName="GeaIos" ReferencedContainer="container:GeaIos.xcodeproj"/>
497
+ </MacroExpansion>
498
+ </TestAction>` : ''}
499
+ <LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
500
+ <BuildableProductRunnable runnableDebuggingMode="0">
501
+ <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="${targetId}" BuildableName="${appName}.app" BlueprintName="GeaIos" ReferencedContainer="container:GeaIos.xcodeproj"/>
502
+ </BuildableProductRunnable>
503
+ </LaunchAction>
504
+ <ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
505
+ <BuildableProductRunnable runnableDebuggingMode="0">
506
+ <BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="${targetId}" BuildableName="${appName}.app" BlueprintName="GeaIos" ReferencedContainer="container:GeaIos.xcodeproj"/>
507
+ </BuildableProductRunnable>
508
+ </ProfileAction>
509
+ <AnalyzeAction buildConfiguration="Debug"/>
510
+ <ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
511
+ </Scheme>
512
+ `
513
+
514
+ fs.writeFileSync(path.join(projectPath, 'project.pbxproj'), pbxproj)
515
+ fs.writeFileSync(
516
+ path.join(projectPath, 'project.xcworkspace/contents.xcworkspacedata'),
517
+ '<?xml version="1.0" encoding="UTF-8"?>\n<Workspace version="1.0"><FileRef location="self:"></FileRef></Workspace>\n',
518
+ )
519
+ fs.writeFileSync(path.join(projectPath, 'xcshareddata/xcschemes/GeaIos.xcscheme'), scheme)
520
+ console.log(`Generated ${projectPath}`)
@@ -0,0 +1,7 @@
1
+ #pragma once
2
+
3
+ #import <UIKit/UIKit.h>
4
+
5
+ @interface GeaCanvasView : UIView
6
+ @property(nonatomic, assign) int nodeId;
7
+ @end
@@ -0,0 +1,94 @@
1
+ #import "canvas_view.h"
2
+
3
+ #include "canvas.h"
4
+ #include "pixel.h"
5
+ #include "ui/tree_internal.h"
6
+
7
+ namespace {
8
+
9
+ std::uint8_t *g_scratch = nullptr;
10
+ std::size_t g_scratchBytes = 0;
11
+
12
+ std::uint8_t *ensureScratch(std::size_t bytes)
13
+ {
14
+ if (g_scratchBytes < bytes) {
15
+ void *grown = std::realloc(g_scratch, bytes);
16
+ if (!grown) return nullptr;
17
+ g_scratch = static_cast<std::uint8_t *>(grown);
18
+ g_scratchBytes = bytes;
19
+ }
20
+ return g_scratch;
21
+ }
22
+
23
+ } // namespace
24
+
25
+ @implementation GeaCanvasView
26
+
27
+ - (instancetype)initWithFrame:(CGRect)frame
28
+ {
29
+ self = [super initWithFrame:frame];
30
+ if (self) {
31
+ _nodeId = -1;
32
+ self.opaque = NO;
33
+ self.backgroundColor = UIColor.clearColor;
34
+ self.userInteractionEnabled = NO;
35
+ }
36
+ return self;
37
+ }
38
+
39
+ - (void)drawRect:(CGRect)dirtyRect
40
+ {
41
+ (void)dirtyRect;
42
+ if (self.nodeId < 0) return;
43
+ auto &tree = gea::embedded::ui::Tree::instance();
44
+ if (self.nodeId >= tree.nodeCount()) return;
45
+ const auto *canvas = gea::embedded::ui::Tree::instance().canvas(self.nodeId);
46
+ if (!canvas) return;
47
+ const gea::framework::graphics::pixel::native_t *src = canvas->pixels();
48
+ const int w = canvas->width();
49
+ const int h = canvas->height();
50
+ if (!src || w <= 0 || h <= 0) return;
51
+
52
+ const std::size_t bytes = static_cast<std::size_t>(w) * h * 4;
53
+ std::uint8_t *dst = ensureScratch(bytes);
54
+ if (!dst) return;
55
+ #if GEA_EMBEDDED_PIXEL_FORMAT == GEA_PIXEL_RGBA8888
56
+ // native_t is already RGBA8888 (R,G,B,A bytes per pixel) — exactly the
57
+ // CGImage's RGBX layout — so present it straight through.
58
+ std::memcpy(dst, src, bytes);
59
+ #else
60
+ for (int i = 0; i < w * h; i++) {
61
+ int r, g, b;
62
+ gea::framework::graphics::pixel::unpackRgb565(src[i], &r, &g, &b);
63
+ dst[i * 4 + 0] = static_cast<std::uint8_t>((r * 255 + 15) / 31);
64
+ dst[i * 4 + 1] = static_cast<std::uint8_t>((g * 255 + 31) / 63);
65
+ dst[i * 4 + 2] = static_cast<std::uint8_t>((b * 255 + 15) / 31);
66
+ dst[i * 4 + 3] = 0xFF;
67
+ }
68
+ #endif
69
+
70
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
71
+ CGDataProviderRef provider = CGDataProviderCreateWithData(nullptr, dst, bytes, nullptr);
72
+ const uint32_t bitmapInfo = static_cast<uint32_t>(kCGImageAlphaNoneSkipLast) |
73
+ static_cast<uint32_t>(kCGBitmapByteOrder32Big);
74
+ CGImageRef image = CGImageCreate(static_cast<std::size_t>(w),
75
+ static_cast<std::size_t>(h),
76
+ 8,
77
+ 32,
78
+ static_cast<std::size_t>(w) * 4,
79
+ cs,
80
+ bitmapInfo,
81
+ provider,
82
+ nullptr,
83
+ false,
84
+ kCGRenderingIntentDefault);
85
+ if (image) {
86
+ UIImage *uiImage = [UIImage imageWithCGImage:image scale:1.0 orientation:UIImageOrientationUp];
87
+ [uiImage drawInRect:self.bounds];
88
+ CGImageRelease(image);
89
+ }
90
+ CGDataProviderRelease(provider);
91
+ CGColorSpaceRelease(cs);
92
+ }
93
+
94
+ @end
@@ -0,0 +1,17 @@
1
+ #pragma once
2
+
3
+ #ifdef __OBJC__
4
+ #import <UIKit/UIKit.h>
5
+ @class UIFont;
6
+ #endif
7
+
8
+ namespace gea::ios {
9
+
10
+ #ifdef __OBJC__
11
+ // Returns a UIKit font for the generated framework font-family id. The id is
12
+ // resolved back to the CSS family name, then matched against fonts registered
13
+ // from the app bundle. Unknown ids fall back to the system font.
14
+ UIFont *fontForId(int fontId, CGFloat sizePt);
15
+ #endif
16
+
17
+ } // namespace gea::ios